diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 95bc65f357b..915496d2438 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -11,7 +11,7 @@ jobs: env: PUPPETEER_SKIP_DOWNLOAD: 'true' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4.1.0 diff --git a/.github/workflows/canary-minor.yml b/.github/workflows/canary-minor.yml index 0b6401b8ce4..8145aa41e36 100644 --- a/.github/workflows/canary-minor.yml +++ b/.github/workflows/canary-minor.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest environment: Release steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: minor diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 71c794c7078..19e38697861 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest environment: Release steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4.1.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8c217f62c4..3f643e50328 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c260a728e71..94e2b49af72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: environment: Release steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4 @@ -36,12 +36,13 @@ jobs: - name: Install deps run: pnpm install + - name: Update npm + run: npm i -g npm@latest + - name: Build and publish id: publish run: | pnpm release --publishOnly - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Create GitHub release id: release_tag diff --git a/.github/workflows/size-data.yml b/.github/workflows/size-data.yml index 5a370b8b92f..2d887377a93 100644 --- a/.github/workflows/size-data.yml +++ b/.github/workflows/size-data.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4.1.0 diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml index 66b5ad0ef29..eba9759c3dc 100644 --- a/.github/workflows/size-report.yml +++ b/.github/workflows/size-report.yml @@ -22,7 +22,7 @@ jobs: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4.1.0 @@ -37,7 +37,7 @@ jobs: run: pnpm install - name: Download Size Data - uses: dawidd6/action-download-artifact@v9 + uses: dawidd6/action-download-artifact@v11 with: name: size-data run_id: ${{ github.event.workflow_run.id }} @@ -56,7 +56,7 @@ jobs: path: temp/size/base.txt - name: Download Previous Size Data - uses: dawidd6/action-download-artifact@v9 + uses: dawidd6/action-download-artifact@v11 with: branch: ${{ steps.pr-base.outputs.content }} workflow: size-data.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1122eb35573..acf4216cfd3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: env: PUPPETEER_SKIP_DOWNLOAD: 'true' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4.1.0 @@ -32,7 +32,7 @@ jobs: env: PUPPETEER_SKIP_DOWNLOAD: 'true' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4.1.0 @@ -54,7 +54,7 @@ jobs: e2e-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup cache for Chromium binary uses: actions/cache@v4 @@ -85,7 +85,7 @@ jobs: env: PUPPETEER_SKIP_DOWNLOAD: 'true' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install pnpm uses: pnpm/action-setup@v4.1.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index d490aa19d95..210af62a698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +## [3.5.19](https://github.com/vuejs/core/compare/v3.5.18...v3.5.19) (2025-08-21) + + +### Bug Fixes + +* **compiler-core:** adjacent v-else should cause a compiler error ([#13699](https://github.com/vuejs/core/issues/13699)) ([911e670](https://github.com/vuejs/core/commit/911e67045e2a63e0ecbd198ed4f567530f6d1c17)), closes [#13698](https://github.com/vuejs/core/issues/13698) +* **compiler-core:** prevent cached array children from retaining detached dom nodes ([#13691](https://github.com/vuejs/core/issues/13691)) ([7f60ef8](https://github.com/vuejs/core/commit/7f60ef83e735dbd29d323347acecf69f22b06d53)), closes [element-plus/element-plus#21408](https://github.com/element-plus/element-plus/issues/21408) [#13211](https://github.com/vuejs/core/issues/13211) +* **compiler-sfc:** improve type inference for generic type aliases types ([#12876](https://github.com/vuejs/core/issues/12876)) ([d9dd628](https://github.com/vuejs/core/commit/d9dd628800ae32e673bdfabfe79f1988037991d0)), closes [#12872](https://github.com/vuejs/core/issues/12872) +* **compiler-sfc:** throw mismatched script langs error before invoking babel ([#13194](https://github.com/vuejs/core/issues/13194)) ([0562548](https://github.com/vuejs/core/commit/0562548ab3a040073386021222225e0e9d43c632)), closes [#13193](https://github.com/vuejs/core/issues/13193) +* **compiler-ssr:** disable v-memo transform in ssr vdom fallback branch ([#13725](https://github.com/vuejs/core/issues/13725)) ([0a202d8](https://github.com/vuejs/core/commit/0a202d890ff2a564b1fab51e4ac621708640818e)), closes [#13724](https://github.com/vuejs/core/issues/13724) +* **devtools:** clear performance measures ([#13701](https://github.com/vuejs/core/issues/13701)) ([c875019](https://github.com/vuejs/core/commit/c875019d49b4c36a88d929ccadc31ad414747c7b)), closes [#13700](https://github.com/vuejs/core/issues/13700) +* **hmr:** prevent updating unmounting component during HMR rerender ([#13775](https://github.com/vuejs/core/issues/13775)) ([6e5143d](https://github.com/vuejs/core/commit/6e5143d9635dac3f20fb394a827109df30e232ae)), closes [#13771](https://github.com/vuejs/core/issues/13771) [#13772](https://github.com/vuejs/core/issues/13772) +* **hydration:** also set vShow name if `__FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__` flag is enabled ([#13777](https://github.com/vuejs/core/issues/13777)) ([439e1a5](https://github.com/vuejs/core/commit/439e1a543e62de4dbf7658d78d05c358c9677c86)), closes [#13744](https://github.com/vuejs/core/issues/13744) +* **reactivity:** warn on nested readonly ref update during unwrapping ([#12141](https://github.com/vuejs/core/issues/12141)) ([1498821](https://github.com/vuejs/core/commit/1498821ed9eeb22a0767e53ddc1f6a2840598a29)) +* **runtime-core:** avoid setting direct ref of useTemplateRef in dev ([#13449](https://github.com/vuejs/core/issues/13449)) ([4a2953f](https://github.com/vuejs/core/commit/4a2953f57b90dfc24e34ff1a87cc1ebb0b97636d)) +* **runtime-core:** improve consistency of `PublicInstanceProxyHandlers.has` ([#13507](https://github.com/vuejs/core/issues/13507)) ([d7283f3](https://github.com/vuejs/core/commit/d7283f3b7f0631c8b8a4a31a05983dac9f078c4f)) +* **suspense:** don't immediately resolve suspense on last dep unmount ([#13456](https://github.com/vuejs/core/issues/13456)) ([a871315](https://github.com/vuejs/core/commit/a8713159ee24602c7c2b70c5fd52d2e5cd37dca5)), closes [#13453](https://github.com/vuejs/core/issues/13453) +* **transition:** handle KeepAlive + transition leaving edge case ([#13152](https://github.com/vuejs/core/issues/13152)) ([3190b17](https://github.com/vuejs/core/commit/3190b179b0545a3dc4549737793eec630cf9f0d1)), closes [#13153](https://github.com/vuejs/core/issues/13153) + + + +## [3.5.18](https://github.com/vuejs/core/compare/v3.5.17...v3.5.18) (2025-07-23) + + +### Bug Fixes + +* **compiler-core:** avoid cached text vnodes retaining detached DOM nodes ([#13662](https://github.com/vuejs/core/issues/13662)) ([00695a5](https://github.com/vuejs/core/commit/00695a5b41b2d032deaeada83831ff83aa6bfd4e)), closes [#13661](https://github.com/vuejs/core/issues/13661) +* **compiler-core:** avoid self updates of `v-pre` ([#12556](https://github.com/vuejs/core/issues/12556)) ([21b685a](https://github.com/vuejs/core/commit/21b685ad9d9d0e6060fc7d07b719bf35f2d9ae1f)) +* **compiler-core:** identifiers in function parameters should not be inferred as references ([#13548](https://github.com/vuejs/core/issues/13548)) ([9b02923](https://github.com/vuejs/core/commit/9b029239edf88558465b941e1e4c085f92b1ebff)) +* **compiler-core:** recognize empty string as non-identifier ([#12553](https://github.com/vuejs/core/issues/12553)) ([ce93339](https://github.com/vuejs/core/commit/ce933390ad1c72bed258f7ad959a78f0e8acdf57)) +* **compiler-core:** transform empty `v-bind` dynamic argument content correctly ([#12554](https://github.com/vuejs/core/issues/12554)) ([d3af67e](https://github.com/vuejs/core/commit/d3af67e878790892f9d34cfea15d13625aabe733)) +* **compiler-sfc:** transform empty srcset w/ includeAbsolute: true ([#13639](https://github.com/vuejs/core/issues/13639)) ([d8e40ef](https://github.com/vuejs/core/commit/d8e40ef7e1c20ee86b294e7cf78e2de60d12830e)), closes [vitejs/vite-plugin-vue#631](https://github.com/vitejs/vite-plugin-vue/issues/631) +* **css-vars:** nullish v-bind in style should not lead to unexpected inheritance ([#12461](https://github.com/vuejs/core/issues/12461)) ([c85f1b5](https://github.com/vuejs/core/commit/c85f1b5a132eb8ec25f71b250e25e65a5c20964f)), closes [#12434](https://github.com/vuejs/core/issues/12434) [#12439](https://github.com/vuejs/core/issues/12439) [#7474](https://github.com/vuejs/core/issues/7474) [#7475](https://github.com/vuejs/core/issues/7475) +* **custom-element:** ensure exposed methods are accessible from custom elements by making them enumerable ([#13634](https://github.com/vuejs/core/issues/13634)) ([90573b0](https://github.com/vuejs/core/commit/90573b06bf6fb6c14c6bbff6c4e34e0ab108953a)), closes [#13632](https://github.com/vuejs/core/issues/13632) +* **hydration:** prevent lazy hydration for updated components ([#13511](https://github.com/vuejs/core/issues/13511)) ([a9269c6](https://github.com/vuejs/core/commit/a9269c642bf944560bc29adb5dae471c11cd9ee8)), closes [#13510](https://github.com/vuejs/core/issues/13510) +* **runtime-core:** ensure correct anchor el for unresolved async components ([#13560](https://github.com/vuejs/core/issues/13560)) ([7f29943](https://github.com/vuejs/core/commit/7f2994393dcdb82cacbf62e02b5ba5565f32588b)), closes [#13559](https://github.com/vuejs/core/issues/13559) +* **slots:** refine internal key checking to support slot names starting with an underscore ([#13612](https://github.com/vuejs/core/issues/13612)) ([c5f7db1](https://github.com/vuejs/core/commit/c5f7db11542bb2246363aef78c88a8e6cef0ee93)), closes [#13611](https://github.com/vuejs/core/issues/13611) +* **ssr:** ensure empty slots render as a comment node in Transition ([#13396](https://github.com/vuejs/core/issues/13396)) ([8cfc10a](https://github.com/vuejs/core/commit/8cfc10a80b9cbf5d801ab149e49b8506d192e7e1)), closes [#13394](https://github.com/vuejs/core/issues/13394) + + + ## [3.5.17](https://github.com/vuejs/core/compare/v3.5.16...v3.5.17) (2025-06-18) diff --git a/package.json b/package.json index b0404823f43..3b23428e63d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, - "version": "3.5.17", - "packageManager": "pnpm@10.12.1", + "version": "3.5.19", + "packageManager": "pnpm@10.15.0", "type": "module", "scripts": { "dev": "node scripts/dev.js", @@ -69,17 +69,17 @@ "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "5.0.4", - "@swc/core": "^1.12.1", + "@swc/core": "^1.13.3", "@types/hash-sum": "^1.0.2", - "@types/node": "^22.15.31", + "@types/node": "^22.17.2", "@types/semver": "^7.7.0", "@types/serve-handler": "^6.1.4", - "@vitest/coverage-v8": "^3.1.4", - "@vitest/eslint-plugin": "^1.2.1", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/eslint-plugin": "^1.3.4", "@vue/consolidate": "1.0.0", "conventional-changelog-cli": "^5.0.0", "enquirer": "^2.4.1", - "esbuild": "^0.25.5", + "esbuild": "^0.25.9", "esbuild-plugin-polyfill-node": "^0.3.0", "eslint": "^9.27.0", "eslint-plugin-import-x": "^4.13.1", @@ -95,10 +95,10 @@ "prettier": "^3.5.3", "pretty-bytes": "^6.1.1", "pug": "^3.0.3", - "puppeteer": "~24.9.0", + "puppeteer": "~24.16.2", "rimraf": "^6.0.1", - "rollup": "^4.43.0", - "rollup-plugin-dts": "^6.2.1", + "rollup": "^4.46.4", + "rollup-plugin-dts": "^6.2.3", "rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-polyfill-node": "^0.13.0", "semver": "^7.7.2", @@ -110,6 +110,6 @@ "typescript": "~5.6.2", "typescript-eslint": "^8.32.1", "vite": "catalog:", - "vitest": "^3.1.4" + "vitest": "^3.2.4" } } diff --git a/packages-private/sfc-playground/package.json b/packages-private/sfc-playground/package.json index 731ff944254..55017ad57a6 100644 --- a/packages-private/sfc-playground/package.json +++ b/packages-private/sfc-playground/package.json @@ -13,7 +13,7 @@ "vite": "catalog:" }, "dependencies": { - "@vue/repl": "^4.6.1", + "@vue/repl": "^4.6.3", "file-saver": "^2.0.5", "jszip": "^3.10.1", "vue": "workspace:*" diff --git a/packages-private/sfc-playground/src/App.vue b/packages-private/sfc-playground/src/App.vue index 455137ba6f6..c0246a7ccf4 100644 --- a/packages-private/sfc-playground/src/App.vue +++ b/packages-private/sfc-playground/src/App.vue @@ -2,7 +2,7 @@ import Header from './Header.vue' import { Repl, useStore, SFCOptions, useVueImportMap } from '@vue/repl' import Monaco from '@vue/repl/monaco-editor' -import { ref, watchEffect, onMounted, computed } from 'vue' +import { ref, watchEffect, onMounted, computed, watch } from 'vue' const replRef = ref>() @@ -115,6 +115,34 @@ onMounted(() => { // @ts-expect-error process shim for old versions of @vue/compiler-sfc dependency window.process = { env: {} } }) + +const isVaporSupported = ref(false) +watch( + () => store.vueVersion, + (version, oldVersion) => { + const [major, minor] = (version || store.compiler.version) + .split('.') + .map((v: string) => parseInt(v, 10)) + isVaporSupported.value = major > 3 || (major === 3 && minor >= 6) + if (oldVersion) reloadPage() + }, + { immediate: true, flush: 'pre' }, +) + +const previewOptions = computed(() => ({ + customCode: { + importCode: `import { initCustomFormatter${isVaporSupported.value ? ', vaporInteropPlugin' : ''} } from 'vue'`, + useCode: ` + ${isVaporSupported.value ? 'app.use(vaporInteropPlugin)' : ''} + 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-private/sfc-playground/src/download/template/package.json b/packages-private/sfc-playground/src/download/template/package.json index b9bb278edf3..8ae34af3f4b 100644 --- a/packages-private/sfc-playground/src/download/template/package.json +++ b/packages-private/sfc-playground/src/download/template/package.json @@ -11,7 +11,7 @@ "vue": "latest" }, "devDependencies": { - "@vitejs/plugin-vue": "^5.2.4", - "vite": "^6.3.5" + "@vitejs/plugin-vue": "^6.0.1", + "vite": "^7.1.3" } } diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap index b8bef22c478..7d5e47719eb 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap @@ -7,9 +7,9 @@ return function render(_ctx, _cache) { with (_ctx) { const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createElementVNode("div", { key: "foo" }, null, -1 /* CACHED */) - ]))) + ]))])) } }" `; @@ -21,7 +21,7 @@ return function render(_ctx, _cache) { with (_ctx) { const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createElementVNode("p", null, [ _createElementVNode("span"), _createElementVNode("span") @@ -30,7 +30,7 @@ return function render(_ctx, _cache) { _createElementVNode("span"), _createElementVNode("span") ], -1 /* CACHED */) - ]))) + ]))])) } }" `; @@ -42,11 +42,11 @@ return function render(_ctx, _cache) { with (_ctx) { const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createElementVNode("div", null, [ _createCommentVNode("comment") ], -1 /* CACHED */) - ]))) + ]))])) } }" `; @@ -58,11 +58,11 @@ return function render(_ctx, _cache) { with (_ctx) { const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createElementVNode("span", null, null, -1 /* CACHED */), - _createTextVNode("foo"), + _createTextVNode("foo", -1 /* CACHED */), _createElementVNode("div", null, null, -1 /* CACHED */) - ]))) + ]))])) } }" `; @@ -74,9 +74,9 @@ return function render(_ctx, _cache) { with (_ctx) { const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createElementVNode("span", { class: "inline" }, "hello", -1 /* CACHED */) - ]))) + ]))])) } }" `; @@ -147,9 +147,9 @@ return function render(_ctx, _cache) { with (_ctx) { const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */) - ]))) + ]))])) } }" `; @@ -161,9 +161,9 @@ return function render(_ctx, _cache) { with (_ctx) { const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */) - ]))) + ]))])) } }" `; @@ -215,9 +215,9 @@ return function render(_ctx, _cache) { const _directive_foo = _resolveDirective("foo") return (_openBlock(), _createElementBlock("div", null, [ - _withDirectives((_openBlock(), _createElementBlock("svg", null, _cache[0] || (_cache[0] = [ + _withDirectives((_openBlock(), _createElementBlock("svg", null, [...(_cache[0] || (_cache[0] = [ _createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* CACHED */) - ]))), [ + ]))])), [ [_directive_foo] ]) ])) @@ -401,9 +401,9 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ ok - ? (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [ + ? (_openBlock(), _createElementBlock("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [ _createElementVNode("span", null, null, -1 /* CACHED */) - ]))) + ]))])) : _createCommentVNode("v-if", true) ])) } @@ -422,7 +422,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock(_Fragment, null, [ _createCommentVNode("comment"), - _createElementVNode("div", _hoisted_1, _cache[0] || (_cache[0] = [ + _createElementVNode("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [ _createElementVNode("div", { id: "b" }, [ _createElementVNode("div", { id: "c" }, [ _createElementVNode("div", { id: "d" }, [ @@ -430,7 +430,7 @@ return function render(_ctx, _cache) { ]) ]) ], -1 /* CACHED */) - ])) + ]))]) ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)) } }" @@ -448,9 +448,9 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => { - return (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [ _createElementVNode("span", null, null, -1 /* CACHED */) - ]))) + ]))])) }), 256 /* UNKEYED_FRAGMENT */)) ])) } diff --git a/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts b/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts index 74f6caca328..1b5b23fec29 100644 --- a/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts +++ b/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts @@ -27,7 +27,7 @@ import { PatchFlags } from '@vue/shared' const cachedChildrenArrayMatcher = ( tags: string[], - needArraySpread = false, + needArraySpread = true, ) => ({ type: NodeTypes.JS_CACHE_EXPRESSION, needArraySpread, @@ -170,11 +170,6 @@ describe('compiler: cacheStatic transform', () => { { /* _ slot flag */ }, - { - type: NodeTypes.JS_PROPERTY, - key: { content: '__' }, - value: { content: '[0]' }, - }, ], }) }) @@ -202,11 +197,6 @@ describe('compiler: cacheStatic transform', () => { { /* _ slot flag */ }, - { - type: NodeTypes.JS_PROPERTY, - key: { content: '__' }, - value: { content: '[0]' }, - }, ], }) }) diff --git a/packages/compiler-core/__tests__/transforms/vIf.spec.ts b/packages/compiler-core/__tests__/transforms/vIf.spec.ts index 2c2fedab0d5..73b6e221554 100644 --- a/packages/compiler-core/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vIf.spec.ts @@ -301,6 +301,25 @@ describe('compiler: v-if', () => { ]) }) + test('error on adjacent v-else', () => { + const onError = vi.fn() + + const { + node: { branches }, + } = parseWithIfTransform( + `
`, + { onError }, + 0, + ) + + expect(onError.mock.calls[0]).toMatchObject([ + { + code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, + loc: branches[branches.length - 1].loc, + }, + ]) + }) + test('error on user key', () => { const onError = vi.fn() // dynamic diff --git a/packages/compiler-core/__tests__/utils.spec.ts b/packages/compiler-core/__tests__/utils.spec.ts index 2d377a271ac..000b10e11bd 100644 --- a/packages/compiler-core/__tests__/utils.spec.ts +++ b/packages/compiler-core/__tests__/utils.spec.ts @@ -1,4 +1,9 @@ -import type { ExpressionNode, TransformContext } from '../src' +import { babelParse, walkIdentifiers } from '@vue/compiler-sfc' +import { + type ExpressionNode, + type TransformContext, + isReferencedIdentifier, +} from '../src' import { type Position, createSimpleExpression } from '../src/ast' import { advancePositionWithClone, @@ -115,3 +120,18 @@ test('toValidAssetId', () => { '_component_test_2797935797_1', ) }) + +describe('isReferencedIdentifier', () => { + test('identifiers in function parameters should not be inferred as references', () => { + expect.assertions(4) + const ast = babelParse(`(({ title }) => [])`) + walkIdentifiers( + ast.program.body[0], + (node, parent, parentStack, isReference) => { + expect(isReference).toBe(false) + expect(isReferencedIdentifier(node, parent, parentStack)).toBe(false) + }, + true, + ) + }) +}) diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index a3c188a6699..a1f5aafff39 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.5.17", + "version": "3.5.19", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", diff --git a/packages/compiler-core/src/babelUtils.ts b/packages/compiler-core/src/babelUtils.ts index 52fabeea896..51614612b10 100644 --- a/packages/compiler-core/src/babelUtils.ts +++ b/packages/compiler-core/src/babelUtils.ts @@ -122,7 +122,7 @@ export function isReferencedIdentifier( return false } - if (isReferenced(id, parent)) { + if (isReferenced(id, parent, parentStack[parentStack.length - 2])) { return true } @@ -132,7 +132,8 @@ export function isReferencedIdentifier( case 'AssignmentExpression': case 'AssignmentPattern': return true - case 'ObjectPattern': + case 'ObjectProperty': + return parent.key !== id && isInDestructureAssignment(parent, parentStack) case 'ArrayPattern': return isInDestructureAssignment(parent, parentStack) } diff --git a/packages/compiler-core/src/parser.ts b/packages/compiler-core/src/parser.ts index 3eb3a976f4e..a6e25681d75 100644 --- a/packages/compiler-core/src/parser.ts +++ b/packages/compiler-core/src/parser.ts @@ -43,6 +43,7 @@ import { isCoreComponent, isSimpleIdentifier, isStaticArgOf, + isVPre, } from './utils' import { decodeHTML } from 'entities/lib/decode.js' import { @@ -246,7 +247,7 @@ const tokenizer = new Tokenizer(stack, { ondirarg(start, end) { if (start === end) return const arg = getSlice(start, end) - if (inVPre) { + if (inVPre && !isVPre(currentProp!)) { ;(currentProp as AttributeNode).name += arg setLocEnd((currentProp as AttributeNode).nameLoc, end) } else { @@ -262,7 +263,7 @@ const tokenizer = new Tokenizer(stack, { ondirmodifier(start, end) { const mod = getSlice(start, end) - if (inVPre) { + if (inVPre && !isVPre(currentProp!)) { ;(currentProp as AttributeNode).name += '.' + mod setLocEnd((currentProp as AttributeNode).nameLoc, end) } else if ((currentProp as DirectiveNode).name === 'slot') { diff --git a/packages/compiler-core/src/transforms/cacheStatic.ts b/packages/compiler-core/src/transforms/cacheStatic.ts index 239ee689a9f..c4d29f71248 100644 --- a/packages/compiler-core/src/transforms/cacheStatic.ts +++ b/packages/compiler-core/src/transforms/cacheStatic.ts @@ -12,19 +12,22 @@ import { type RootNode, type SimpleExpressionNode, type SlotFunctionExpression, - type SlotsObjectProperty, type TemplateChildNode, type TemplateNode, type TextCallNode, type VNodeCall, createArrayExpression, - createObjectProperty, - createSimpleExpression, getVNodeBlockHelper, getVNodeHelper, } from '../ast' import type { TransformContext } from '../transform' -import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared' +import { + PatchFlagNames, + PatchFlags, + isArray, + isString, + isSymbol, +} from '@vue/shared' import { findDir, isSlotOutlet } from '../utils' import { GUARD_REACTIVE_PROPS, @@ -109,6 +112,15 @@ function walk( ? ConstantTypes.NOT_CONSTANT : getConstantType(child, context) if (constantType >= ConstantTypes.CAN_CACHE) { + if ( + child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION && + child.codegenNode.arguments.length > 0 + ) { + child.codegenNode.arguments.push( + PatchFlags.CACHED + + (__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``), + ) + } toCache.push(child) continue } @@ -142,7 +154,6 @@ function walk( } let cachedAsArray = false - const slotCacheKeys = [] if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) { if ( node.tagType === ElementTypes.ELEMENT && @@ -166,7 +177,6 @@ function walk( // default slot const slot = getSlotNode(node.codegenNode, 'default') if (slot) { - slotCacheKeys.push(context.cached.length) slot.returns = getCacheExpression( createArrayExpression(slot.returns as TemplateChildNode[]), ) @@ -190,7 +200,6 @@ function walk( slotName.arg && getSlotNode(parent.codegenNode, slotName.arg) if (slot) { - slotCacheKeys.push(context.cached.length) slot.returns = getCacheExpression( createArrayExpression(slot.returns as TemplateChildNode[]), ) @@ -201,39 +210,22 @@ function walk( if (!cachedAsArray) { for (const child of toCache) { - slotCacheKeys.push(context.cached.length) child.codegenNode = context.cache(child.codegenNode!) } } - // put the slot cached keys on the slot object, so that the cache - // can be removed when component unmounting to prevent memory leaks - if ( - slotCacheKeys.length && - node.type === NodeTypes.ELEMENT && - node.tagType === ElementTypes.COMPONENT && - node.codegenNode && - node.codegenNode.type === NodeTypes.VNODE_CALL && - node.codegenNode.children && - !isArray(node.codegenNode.children) && - node.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION - ) { - node.codegenNode.children.properties.push( - createObjectProperty( - `__`, - createSimpleExpression(JSON.stringify(slotCacheKeys), false), - ) as SlotsObjectProperty, - ) - } - function getCacheExpression(value: JSChildNode): CacheExpression { const exp = context.cache(value) // #6978, #7138, #7114 // a cached children array inside v-for can caused HMR errors since // it might be mutated when mounting the first item - if (inFor && context.hmr) { - exp.needArraySpread = true - } + // #13221 + // fix memory leak in cached array: + // cached vnodes get replaced by cloned ones during mountChildren, + // which bind DOM elements. These DOM references persist after unmount, + // preventing garbage collection. Array spread avoids mutating cached + // array, preventing memory leaks. + exp.needArraySpread = true return exp } diff --git a/packages/compiler-core/src/transforms/vBind.ts b/packages/compiler-core/src/transforms/vBind.ts index 1e5e371418b..c82706c10c7 100644 --- a/packages/compiler-core/src/transforms/vBind.ts +++ b/packages/compiler-core/src/transforms/vBind.ts @@ -65,7 +65,7 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => { arg.children.unshift(`(`) arg.children.push(`) || ""`) } else if (!arg.isStatic) { - arg.content = `${arg.content} || ""` + arg.content = arg.content ? `${arg.content} || ""` : `""` } // .sync is replaced by v-model:arg diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index 54c505407a3..8bf5c6a32ff 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -141,9 +141,9 @@ export function processIf( } if (sibling && sibling.type === NodeTypes.IF) { - // Check if v-else was followed by v-else-if + // Check if v-else was followed by v-else-if or there are two adjacent v-else if ( - dir.name === 'else-if' && + (dir.name === 'else-if' || dir.name === 'else') && sibling.branches[sibling.branches.length - 1].condition === undefined ) { context.onError( diff --git a/packages/compiler-core/src/transforms/vMemo.ts b/packages/compiler-core/src/transforms/vMemo.ts index 83295a45c8a..31db1448af0 100644 --- a/packages/compiler-core/src/transforms/vMemo.ts +++ b/packages/compiler-core/src/transforms/vMemo.ts @@ -16,7 +16,7 @@ const seen = new WeakSet() export const transformMemo: NodeTransform = (node, context) => { if (node.type === NodeTypes.ELEMENT) { const dir = findDir(node, 'memo') - if (!dir || seen.has(node)) { + if (!dir || seen.has(node) || context.inSSR) { return } seen.add(node) diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index b49d70bb2fb..ab851ed6f69 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -63,7 +63,7 @@ export function isCoreComponent(tag: string): symbol | void { } } -const nonIdentifierRE = /^\d|[^\$\w\xA0-\uFFFF]/ +const nonIdentifierRE = /^$|^\d|[^\$\w\xA0-\uFFFF]/ export const isSimpleIdentifier = (name: string): boolean => !nonIdentifierRE.test(name) @@ -343,6 +343,10 @@ export function isText( return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT } +export function isVPre(p: ElementNode['props'][0]): p is DirectiveNode { + return p.type === NodeTypes.DIRECTIVE && p.name === 'pre' +} + export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode { return p.type === NodeTypes.DIRECTIVE && p.name === 'slot' } diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap index 5bc40d3fab5..84c3024f6bf 100644 --- a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap +++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap @@ -4,11 +4,11 @@ exports[`stringify static html > eligible content (elements > 20) + non-eligible "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue return function render(_ctx, _cache) { - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createStaticVNode("", 20), _createElementVNode("div", { key: "1" }, "1", -1 /* CACHED */), _createStaticVNode("", 20) - ]))) + ]))])) }" `; @@ -16,9 +16,9 @@ exports[`stringify static html > escape 1`] = ` "const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue return function render(_ctx, _cache) { - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createStaticVNode("
1 + <&1 + <&1 + <&1 + <&1 + <&
", 1) - ]))) + ]))])) }" `; @@ -26,9 +26,9 @@ exports[`stringify static html > serializing constant bindings 1`] = ` "const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue return function render(_ctx, _cache) { - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createStaticVNode("
1 + false1 + false1 + false1 + false1 + false
", 1) - ]))) + ]))])) }" `; @@ -36,9 +36,9 @@ exports[`stringify static html > serializing template string style 1`] = ` "const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue return function render(_ctx, _cache) { - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createStaticVNode("
1 + false1 + false1 + false1 + false1 + false
", 1) - ]))) + ]))])) }" `; @@ -46,7 +46,7 @@ exports[`stringify static html > should bail for