Merge tag 'v3.5.13'

This commit is contained in:
三咲智子 Kevin Deng 2024-11-16 02:48:30 +08:00
commit f6ce3f9d43
No known key found for this signature in database
69 changed files with 1061 additions and 121 deletions

View File

@ -14,5 +14,6 @@
}, },
"[json]": { "[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} },
"editor.formatOnSave": true
} }

View File

@ -0,0 +1 @@
https://vuejs.org/funding.json

View File

@ -1,3 +1,36 @@
## [3.5.13](https://github.com/vuejs/core/compare/v3.5.12...v3.5.13) (2024-11-15)
### Bug Fixes
* **compiler-core:** handle v-memo + v-for with functional key ([#12014](https://github.com/vuejs/core/issues/12014)) ([99009ee](https://github.com/vuejs/core/commit/99009eee0efc238392daba93792d478525b21afa)), closes [#12013](https://github.com/vuejs/core/issues/12013)
* **compiler-dom:** properly stringify template string style ([#12392](https://github.com/vuejs/core/issues/12392)) ([2d78539](https://github.com/vuejs/core/commit/2d78539da35322aea5f821b3cf9b02d006abac72)), closes [#12391](https://github.com/vuejs/core/issues/12391)
* **custom-element:** avoid triggering mutationObserver when relecting props ([352bc88](https://github.com/vuejs/core/commit/352bc88c1bd2fda09c61ab17ea1a5967ffcd7bc0)), closes [#12214](https://github.com/vuejs/core/issues/12214) [#12215](https://github.com/vuejs/core/issues/12215)
* **deps:** update dependency postcss to ^8.4.48 ([#12356](https://github.com/vuejs/core/issues/12356)) ([b5ff930](https://github.com/vuejs/core/commit/b5ff930089985a58c3553977ef999cec2a6708a4))
* **hydration:** the component vnode's el should be updated when a mismatch occurs. ([#12255](https://github.com/vuejs/core/issues/12255)) ([a20a4cb](https://github.com/vuejs/core/commit/a20a4cb36a3e717d1f8f259d0d59f133f508ff0a)), closes [#12253](https://github.com/vuejs/core/issues/12253)
* **reactiivty:** avoid unnecessary watcher effect removal from inactive scope ([2193284](https://github.com/vuejs/core/commit/21932840eae72ffcd357a62ec596aaecc7ec224a)), closes [#5783](https://github.com/vuejs/core/issues/5783) [#5806](https://github.com/vuejs/core/issues/5806)
* **reactivity:** release nested effects/scopes on effect scope stop ([#12373](https://github.com/vuejs/core/issues/12373)) ([bee2f5e](https://github.com/vuejs/core/commit/bee2f5ee62dc0cd04123b737779550726374dd0a)), closes [#12370](https://github.com/vuejs/core/issues/12370)
* **runtime-dom:** set css vars before user onMounted hooks ([2d5c5e2](https://github.com/vuejs/core/commit/2d5c5e25e9b7a56e883674fb434135ac514429b5)), closes [#11533](https://github.com/vuejs/core/issues/11533)
* **runtime-dom:** set css vars on update to handle child forcing reflow in onMount ([#11561](https://github.com/vuejs/core/issues/11561)) ([c4312f9](https://github.com/vuejs/core/commit/c4312f9c715c131a09e552ba46e9beb4b36d55e6))
* **ssr:** avoid updating subtree of async component if it is resolved ([#12363](https://github.com/vuejs/core/issues/12363)) ([da7ad5e](https://github.com/vuejs/core/commit/da7ad5e3d24f3e108401188d909d27a4910da095)), closes [#12362](https://github.com/vuejs/core/issues/12362)
* **ssr:** ensure v-text updates correctly with custom directives in SSR output ([#12311](https://github.com/vuejs/core/issues/12311)) ([1f75d4e](https://github.com/vuejs/core/commit/1f75d4e6dfe18121ebe443cd3e8105d54f727893)), closes [#12309](https://github.com/vuejs/core/issues/12309)
* **ssr:** handle initial selected state for select with v-model + v-for option ([#12399](https://github.com/vuejs/core/issues/12399)) ([4f8d807](https://github.com/vuejs/core/commit/4f8d8078221ee52deed266677a227ad2a6d8dd22)), closes [#12395](https://github.com/vuejs/core/issues/12395)
* **teleport:** handle deferred teleport update before mounted ([#12168](https://github.com/vuejs/core/issues/12168)) ([8bff142](https://github.com/vuejs/core/commit/8bff142f99b646e9dd15897ec75368fbf34f1534)), closes [#12161](https://github.com/vuejs/core/issues/12161)
* **templateRef:** set ref on cached async component which wrapped in KeepAlive ([#12290](https://github.com/vuejs/core/issues/12290)) ([983eb50](https://github.com/vuejs/core/commit/983eb50a17eac76f1bba4394ad0316c62b72191d)), closes [#4999](https://github.com/vuejs/core/issues/4999) [#5004](https://github.com/vuejs/core/issues/5004)
* **test:** update snapshot ([#12169](https://github.com/vuejs/core/issues/12169)) ([828d4a4](https://github.com/vuejs/core/commit/828d4a443919fa2aa4e2e92fbd03a5f04b258eea))
* **Transition:** fix transition memory leak edge case ([#12182](https://github.com/vuejs/core/issues/12182)) ([660132d](https://github.com/vuejs/core/commit/660132df6c6a8c14bf75e593dc47d2fdada30322)), closes [#12181](https://github.com/vuejs/core/issues/12181)
* **transition:** reflow before leave-active class after leave-from ([#12288](https://github.com/vuejs/core/issues/12288)) ([4b479db](https://github.com/vuejs/core/commit/4b479db61d233b054561402ae94ef08550073ea1)), closes [#2593](https://github.com/vuejs/core/issues/2593)
* **types:** defineEmits w/ interface declaration ([#12343](https://github.com/vuejs/core/issues/12343)) ([1022eab](https://github.com/vuejs/core/commit/1022eabaa1aaf8436876f5ec5573cb1e4b3959a6)), closes [#8457](https://github.com/vuejs/core/issues/8457)
* **v-once:** setting hasOnce to current block only when in v-once ([#12374](https://github.com/vuejs/core/issues/12374)) ([37300fc](https://github.com/vuejs/core/commit/37300fc26190a7299efddbf98800ffd96d5cad96)), closes [#12371](https://github.com/vuejs/core/issues/12371)
### Performance Improvements
* **reactivity:** do not track inner key `__v_skip`` ([#11690](https://github.com/vuejs/core/issues/11690)) ([d637bd6](https://github.com/vuejs/core/commit/d637bd6c0164c2883e6eabd3c2f1f8c258dedfb1))
* **runtime-core:** use feature flag for call to resolveMergedOptions ([#12163](https://github.com/vuejs/core/issues/12163)) ([1755ac0](https://github.com/vuejs/core/commit/1755ac0a108ba3486bd8397e56d3bdcd69196594))
## [3.5.12](https://github.com/vuejs/core/compare/v3.5.11...v3.5.12) (2024-10-11) ## [3.5.12](https://github.com/vuejs/core/compare/v3.5.11...v3.5.12) (2024-10-11)

View File

@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "3.5.12", "version": "3.5.13",
"packageManager": "pnpm@9.12.3", "packageManager": "pnpm@9.12.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -22,7 +22,10 @@
"test-dts": "run-s build-dts test-dts-only", "test-dts": "run-s build-dts test-dts-only",
"test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json", "test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json",
"test-coverage": "vitest run --project unit --coverage", "test-coverage": "vitest run --project unit --coverage",
"test-bench": "vitest bench", "prebench": "node scripts/build.js -pf esm-browser reactivity",
"prebench-compare": "node scripts/build.js -pf esm-browser reactivity",
"bench": "vitest bench --project=unit --outputJson=temp/bench.json",
"bench-compare": "vitest bench --project=unit --compare=temp/bench.json",
"release": "node scripts/release.js", "release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"dev-esm": "node scripts/dev.js -if esm-bundler-runtime", "dev-esm": "node scripts/dev.js -if esm-bundler-runtime",

View File

@ -306,6 +306,14 @@ describe('defineEmits w/ type declaration', () => {
emit2('baz') emit2('baz')
}) })
describe('defineEmits w/ interface declaration', () => {
interface Emits {
foo: [value: string]
}
const emit = defineEmits<Emits>()
emit('foo', 'hi')
})
describe('defineEmits w/ alt type declaration', () => { describe('defineEmits w/ alt type declaration', () => {
const emit = defineEmits<{ const emit = defineEmits<{
foo: [id: string] foo: [id: string]

View File

@ -48,6 +48,7 @@ function resetVueVersion() {
async function copyLink(e: MouseEvent) { async function copyLink(e: MouseEvent) {
if (e.metaKey) { if (e.metaKey) {
resetVueVersion()
// hidden logic for going to local debug from play.vuejs.org // hidden logic for going to local debug from play.vuejs.org
window.location.href = 'http://localhost:5173/' + window.location.hash window.location.href = 'http://localhost:5173/' + window.location.hash
return return

View File

@ -1,5 +1,23 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-memo transform > element v-for key expression prefixing + v-memo 1`] = `
"import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, isMemoSame as _isMemoSame, withMemo as _withMemo } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.tableData, (data, __, ___, _cached) => {
const _memo = (_ctx.getLetter(data))
if (_cached && _cached.key === _ctx.getId(data) && _isMemoSame(_cached, _memo)) return _cached
const _item = (_openBlock(), _createElementBlock("span", {
key: _ctx.getId(data)
}))
_item.memo = _memo
return _item
}, _cache, 0), 128 /* KEYED_FRAGMENT */))
]))
}"
`;
exports[`compiler: v-memo transform > on component 1`] = ` exports[`compiler: v-memo transform > on component 1`] = `
"import { resolveComponent as _resolveComponent, createVNode as _createVNode, withMemo as _withMemo, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" "import { resolveComponent as _resolveComponent, createVNode as _createVNode, withMemo as _withMemo, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

View File

@ -8,7 +8,7 @@ return function render(_ctx, _cache) {
const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode } = _Vue const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode } = _Vue
return _cache[0] || ( return _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1, true),
(_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0, (_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1), _setBlockTracking(1),
_cache[0] _cache[0]
@ -28,7 +28,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1, true),
(_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0, (_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1), _setBlockTracking(1),
_cache[0] _cache[0]
@ -47,7 +47,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1, true),
(_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0, (_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1), _setBlockTracking(1),
_cache[0] _cache[0]
@ -66,7 +66,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1, true),
(_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0, (_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0,
_setBlockTracking(1), _setBlockTracking(1),
_cache[0] _cache[0]
@ -85,7 +85,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1, true),
(_cache[0] = _createElementVNode("div")).cacheIndex = 0, (_cache[0] = _createElementVNode("div")).cacheIndex = 0,
_setBlockTracking(1), _setBlockTracking(1),
_cache[0] _cache[0]

View File

@ -53,4 +53,12 @@ describe('compiler: v-memo transform', () => {
), ),
).toMatchSnapshot() ).toMatchSnapshot()
}) })
test('element v-for key expression prefixing + v-memo', () => {
expect(
compile(
`<span v-for="data of tableData" :key="getId(data)" v-memo="getLetter(data)"></span>`,
),
).toMatchSnapshot()
})
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-core", "name": "@vue/compiler-core",
"version": "3.5.12", "version": "3.5.13",
"description": "@vue/compiler-core", "description": "@vue/compiler-core",
"main": "index.js", "main": "index.js",
"module": "dist/compiler-core.esm-bundler.js", "module": "dist/compiler-core.esm-bundler.js",

View File

@ -425,6 +425,7 @@ export interface CacheExpression extends Node {
index: number index: number
value: JSChildNode value: JSChildNode
needPauseTracking: boolean needPauseTracking: boolean
inVOnce: boolean
needArraySpread: boolean needArraySpread: boolean
} }
@ -781,12 +782,14 @@ export function createCacheExpression(
index: number, index: number,
value: JSChildNode, value: JSChildNode,
needPauseTracking: boolean = false, needPauseTracking: boolean = false,
inVOnce: boolean = false,
): CacheExpression { ): CacheExpression {
return { return {
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index, index,
value, value,
needPauseTracking: needPauseTracking, needPauseTracking: needPauseTracking,
inVOnce,
needArraySpread: false, needArraySpread: false,
loc: locStub, loc: locStub,
} }

View File

@ -1034,7 +1034,9 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
push(`_cache[${node.index}] || (`) push(`_cache[${node.index}] || (`)
if (needPauseTracking) { if (needPauseTracking) {
indent() indent()
push(`${helper(SET_BLOCK_TRACKING)}(-1),`) push(`${helper(SET_BLOCK_TRACKING)}(-1`)
if (node.inVOnce) push(`, true`)
push(`),`)
newline() newline()
push(`(`) push(`(`)
} }

View File

@ -116,7 +116,7 @@ export interface TransformContext
addIdentifiers(exp: ExpressionNode | string): void addIdentifiers(exp: ExpressionNode | string): void
removeIdentifiers(exp: ExpressionNode | string): void removeIdentifiers(exp: ExpressionNode | string): void
hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode
cache(exp: JSChildNode, isVNode?: boolean): CacheExpression cache(exp: JSChildNode, isVNode?: boolean, inVOnce?: boolean): CacheExpression
constantCache: WeakMap<TemplateChildNode, ConstantTypes> constantCache: WeakMap<TemplateChildNode, ConstantTypes>
// 2.x Compat only // 2.x Compat only
@ -297,11 +297,12 @@ export function createTransformContext(
identifier.hoisted = exp identifier.hoisted = exp
return identifier return identifier
}, },
cache(exp, isVNode = false) { cache(exp, isVNode = false, inVOnce = false) {
const cacheExp = createCacheExpression( const cacheExp = createCacheExpression(
context.cached.length, context.cached.length,
exp, exp,
isVNode, isVNode,
inVOnce,
) )
context.cached.push(cacheExp) context.cached.push(cacheExp)
return cacheExp return cacheExp

View File

@ -24,7 +24,7 @@ import {
isStaticPropertyKey, isStaticPropertyKey,
walkIdentifiers, walkIdentifiers,
} from '../babelUtils' } from '../babelUtils'
import { advancePositionWithClone, isSimpleIdentifier } from '../utils' import { advancePositionWithClone, findDir, isSimpleIdentifier } from '../utils'
import { import {
genPropsAccessExp, genPropsAccessExp,
hasOwn, hasOwn,
@ -55,6 +55,7 @@ export const transformExpression: NodeTransform = (node, context) => {
) )
} else if (node.type === NodeTypes.ELEMENT) { } else if (node.type === NodeTypes.ELEMENT) {
// handle directives on element // handle directives on element
const memo = findDir(node, 'memo')
for (let i = 0; i < node.props.length; i++) { for (let i = 0; i < node.props.length; i++) {
const dir = node.props[i] const dir = node.props[i]
// do not process for v-on & v-for since they are special handled // do not process for v-on & v-for since they are special handled
@ -66,7 +67,14 @@ export const transformExpression: NodeTransform = (node, context) => {
if ( if (
exp && exp &&
exp.type === NodeTypes.SIMPLE_EXPRESSION && exp.type === NodeTypes.SIMPLE_EXPRESSION &&
!(dir.name === 'on' && arg) !(dir.name === 'on' && arg) &&
// key has been processed in transformFor(vMemo + vFor)
!(
memo &&
arg &&
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
arg.content === 'key'
)
) { ) {
dir.exp = processExpression( dir.exp = processExpression(
exp, exp,

View File

@ -63,17 +63,27 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
const isTemplate = isTemplateNode(node) const isTemplate = isTemplateNode(node)
const memo = findDir(node, 'memo') const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`, false, true) const keyProp = findProp(node, `key`, false, true)
if (keyProp && keyProp.type === NodeTypes.DIRECTIVE && !keyProp.exp) { const isDirKey = keyProp && keyProp.type === NodeTypes.DIRECTIVE
if (isDirKey && !keyProp.exp) {
// resolve :key shorthand #10882 // resolve :key shorthand #10882
transformBindShorthand(keyProp, context) transformBindShorthand(keyProp, context)
} }
const keyExp = let keyExp =
keyProp && keyProp &&
(keyProp.type === NodeTypes.ATTRIBUTE (keyProp.type === NodeTypes.ATTRIBUTE
? keyProp.value ? keyProp.value
? createSimpleExpression(keyProp.value.content, true) ? createSimpleExpression(keyProp.value.content, true)
: undefined : undefined
: keyProp.exp) : keyProp.exp)
if (memo && keyExp && isDirKey) {
if (!__BROWSER__) {
keyProp.exp = keyExp = processExpression(
keyExp as SimpleExpressionNode,
context,
)
}
}
const keyProperty = const keyProperty =
keyProp && keyExp ? createObjectProperty(`key`, keyExp) : null keyProp && keyExp ? createObjectProperty(`key`, keyExp) : null

View File

@ -17,7 +17,11 @@ export const transformOnce: NodeTransform = (node, context) => {
context.inVOnce = false context.inVOnce = false
const cur = context.currentNode as ElementNode | IfNode | ForNode const cur = context.currentNode as ElementNode | IfNode | ForNode
if (cur.codegenNode) { if (cur.codegenNode) {
cur.codegenNode = context.cache(cur.codegenNode, true /* isVNode */) cur.codegenNode = context.cache(
cur.codegenNode,
true /* isVNode */,
true /* inVOnce */,
)
} }
} }
} }

View File

@ -32,6 +32,16 @@ return function render(_ctx, _cache) {
}" }"
`; `;
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] = [
_createStaticVNode("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1)
])))
}"
`;
exports[`stringify static html > should bail for <option> elements with null values 1`] = ` exports[`stringify static html > should bail for <option> elements with null values 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

View File

@ -162,6 +162,27 @@ describe('stringify static html', () => {
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
// #12391
test('serializing template string style', () => {
const { ast, code } = compileWithStringify(
`<div><div :style="\`color:red;\`">${repeat(
`<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`,
)
// should be optimized now
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
`<div style="color:red;">${repeat(
`<span class="foo bar">1 + false</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
1,
),
])
expect(code).toMatchSnapshot()
})
test('escape', () => { test('escape', () => {
const { ast, code } = compileWithStringify( const { ast, code } = compileWithStringify(
`<div><div>${repeat( `<div><div>${repeat(

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-dom", "name": "@vue/compiler-dom",
"version": "3.5.12", "version": "3.5.13",
"description": "@vue/compiler-dom", "description": "@vue/compiler-dom",
"main": "index.js", "main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js", "module": "dist/compiler-dom.esm-bundler.js",

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-sfc", "name": "@vue/compiler-sfc",
"version": "3.5.12", "version": "3.5.13",
"description": "@vue/compiler-sfc", "description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js", "main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js", "module": "dist/compiler-sfc.esm-browser.js",
@ -61,7 +61,7 @@
"merge-source-map": "^1.1.0", "merge-source-map": "^1.1.0",
"minimatch": "~9.0.5", "minimatch": "~9.0.5",
"postcss-modules": "^6.0.0", "postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.1.2", "postcss-selector-parser": "^7.0.0",
"pug": "^3.0.3", "pug": "^3.0.3",
"sass": "^1.80.6" "sass": "^1.80.6"
} }

View File

@ -189,8 +189,7 @@ function rewriteSelector(
// global: replace with inner selector and do not inject [id]. // global: replace with inner selector and do not inject [id].
// ::v-global(.foo) -> .foo // ::v-global(.foo) -> .foo
if (value === ':global' || value === '::v-global') { if (value === ':global' || value === '::v-global') {
selectorRoot.insertAfter(selector, n.nodes[0]) selector.replaceWith(n.nodes[0])
selectorRoot.removeChild(selector)
return false return false
} }
} }

View File

@ -337,6 +337,39 @@ describe('ssr: element', () => {
`) `)
}) })
test('custom dir with v-text', () => {
expect(getCompiledString(`<div v-xxx v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('custom dir with v-text and normal attrs', () => {
expect(getCompiledString(`<div class="test" v-xxx v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({ class: "test" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('mulptiple custom dirs with v-text', () => {
expect(getCompiledString(`<div v-xxx v-yyy v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps(_ssrGetDirectiveProps(_ctx, _directive_xxx), _ssrGetDirectiveProps(_ctx, _directive_yyy)))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('custom dir with object v-bind', () => { test('custom dir with object v-bind', () => {
expect(getCompiledString(`<div v-bind="x" v-xxx />`)) expect(getCompiledString(`<div v-bind="x" v-xxx />`))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`

View File

@ -52,6 +52,52 @@ describe('ssr: v-model', () => {
}" }"
`) `)
expect(
compileWithWrapper(
`<select v-model="model"><option v-for="i in items" :value="i"></option></select>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}><select><!--[-->\`)
_ssrRenderList(_ctx.items, (i) => {
_push(\`<option\${
_ssrRenderAttr("value", i)
}\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, i)
: _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
}></option>\`)
})
_push(\`<!--]--></select></div>\`)
}"
`)
expect(
compileWithWrapper(
`<select v-model="model"><option v-if="true" :value="i"></option></select>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}><select>\`)
if (true) {
_push(\`<option\${
_ssrRenderAttr("value", _ctx.i)
}\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, _ctx.i)
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
}></option>\`)
} else {
_push(\`<!---->\`)
}
_push(\`</select></div>\`)
}"
`)
expect( expect(
compileWithWrapper( compileWithWrapper(
`<select multiple v-model="model"><option value="1" selected></option><option value="2"></option></select>`, `<select multiple v-model="model"><option value="1" selected></option><option value="2"></option></select>`,

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-ssr", "name": "@vue/compiler-ssr",
"version": "3.5.12", "version": "3.5.13",
"description": "@vue/compiler-ssr", "description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js", "main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts", "types": "dist/compiler-ssr.d.ts",

View File

@ -28,6 +28,7 @@ import {
createSequenceExpression, createSequenceExpression,
createSimpleExpression, createSimpleExpression,
createTemplateLiteral, createTemplateLiteral,
findDir,
hasDynamicKeyVBind, hasDynamicKeyVBind,
isStaticArgOf, isStaticArgOf,
isStaticExp, isStaticExp,
@ -164,6 +165,9 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
] ]
} }
} else if (directives.length && !node.children.length) { } else if (directives.length && !node.children.length) {
// v-text directive has higher priority than the merged props
const vText = findDir(node, 'text')
if (!vText) {
const tempId = `_temp${context.temps++}` const tempId = `_temp${context.temps++}`
propsExp.arguments = [ propsExp.arguments = [
createAssignmentExpression( createAssignmentExpression(
@ -183,6 +187,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
), ),
) )
} }
}
if (needTagForRuntime) { if (needTagForRuntime) {
propsExp.arguments.push(`"${node.tag}"`) propsExp.arguments.push(`"${node.tag}"`)

View File

@ -5,6 +5,7 @@ import {
type ExpressionNode, type ExpressionNode,
NodeTypes, NodeTypes,
type PlainElementNode, type PlainElementNode,
type TemplateChildNode,
createCallExpression, createCallExpression,
createConditionalExpression, createConditionalExpression,
createDOMCompilerError, createDOMCompilerError,
@ -162,11 +163,18 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
checkDuplicatedValue() checkDuplicatedValue()
node.children = [createInterpolation(model, model.loc)] node.children = [createInterpolation(model, model.loc)]
} else if (node.tag === 'select') { } else if (node.tag === 'select') {
node.children.forEach(child => { const processChildren = (children: TemplateChildNode[]) => {
children.forEach(child => {
if (child.type === NodeTypes.ELEMENT) { if (child.type === NodeTypes.ELEMENT) {
processOption(child as PlainElementNode) processOption(child as PlainElementNode)
} else if (child.type === NodeTypes.FOR) {
processChildren(child.children)
} else if (child.type === NodeTypes.IF) {
child.branches.forEach(b => processChildren(b.children))
} }
}) })
}
processChildren(node.children)
} else { } else {
context.onError( context.onError(
createDOMCompilerError( createDOMCompilerError(

View File

@ -1,5 +1,10 @@
import { bench, describe } from 'vitest' import { bench, describe } from 'vitest'
import { type ComputedRef, type Ref, computed, effect, ref } from '../src' import type { ComputedRef, Ref } from '../src'
import { computed, effect, ref } from '../dist/reactivity.esm-browser.prod'
declare module '../dist/reactivity.esm-browser.prod' {
function computed(...args: any[]): any
}
describe('computed', () => { describe('computed', () => {
bench('create computed', () => { bench('create computed', () => {

View File

@ -1,5 +1,6 @@
import { bench, describe } from 'vitest' import { bench, describe } from 'vitest'
import { type Ref, effect, ref } from '../src' import type { Ref } from '../src'
import { effect, ref } from '../dist/reactivity.esm-browser.prod'
describe('effect', () => { describe('effect', () => {
{ {

View File

@ -1,5 +1,9 @@
import { bench } from 'vitest' import { bench } from 'vitest'
import { effect, reactive, shallowReadArray } from '../src' import {
effect,
reactive,
shallowReadArray,
} from '../dist/reactivity.esm-browser.prod'
for (let amount = 1e1; amount < 1e4; amount *= 10) { for (let amount = 1e1; amount < 1e4; amount *= 10) {
{ {

View File

@ -1,5 +1,6 @@
import { bench } from 'vitest' import { bench } from 'vitest'
import { type ComputedRef, computed, reactive } from '../src' import type { ComputedRef } from '../src'
import { computed, reactive } from '../dist/reactivity.esm-browser.prod'
function createMap(obj: Record<string, any>) { function createMap(obj: Record<string, any>) {
const map = new Map() const map = new Map()

View File

@ -1,5 +1,5 @@
import { bench } from 'vitest' import { bench } from 'vitest'
import { reactive } from '../src' import { reactive } from '../dist/reactivity.esm-browser.prod'
bench('create reactive obj', () => { bench('create reactive obj', () => {
reactive({ a: 1 }) reactive({ a: 1 })

View File

@ -1,5 +1,5 @@
import { bench, describe } from 'vitest' import { bench, describe } from 'vitest'
import { ref } from '../src/index' import { ref } from '../dist/reactivity.esm-browser.prod'
describe('ref', () => { describe('ref', () => {
bench('create ref', () => { bench('create ref', () => {

View File

@ -176,7 +176,7 @@ describe('reactivity/effect/scope', () => {
expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned() expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned()
expect(scope.effects.length).toBe(1) expect(scope.effects.length).toBe(0)
counter.num = 7 counter.num = 7
expect(dummy).toBe(0) expect(dummy).toBe(0)
@ -322,4 +322,44 @@ describe('reactivity/effect/scope', () => {
scope.resume() scope.resume()
expect(fnSpy).toHaveBeenCalledTimes(3) expect(fnSpy).toHaveBeenCalledTimes(3)
}) })
test('removing a watcher while stopping its effectScope', async () => {
const count = ref(0)
const scope = effectScope()
let watcherCalls = 0
let cleanupCalls = 0
scope.run(() => {
const stop1 = watch(count, () => {
watcherCalls++
})
watch(count, (val, old, onCleanup) => {
watcherCalls++
onCleanup(() => {
cleanupCalls++
stop1()
})
})
watch(count, () => {
watcherCalls++
})
})
expect(watcherCalls).toBe(0)
expect(cleanupCalls).toBe(0)
count.value++
await nextTick()
expect(watcherCalls).toBe(3)
expect(cleanupCalls).toBe(0)
scope.stop()
count.value++
await nextTick()
expect(watcherCalls).toBe(3)
expect(cleanupCalls).toBe(1)
expect(scope.effects.length).toBe(0)
expect(scope.cleanups.length).toBe(0)
})
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/reactivity", "name": "@vue/reactivity",
"version": "3.5.12", "version": "3.5.13",
"description": "@vue/reactivity", "description": "@vue/reactivity",
"main": "index.js", "main": "index.js",
"module": "dist/reactivity.esm-bundler.js", "module": "dist/reactivity.esm-bundler.js",

View File

@ -53,6 +53,8 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
) {} ) {}
get(target: Target, key: string | symbol, receiver: object): any { get(target: Target, key: string | symbol, receiver: object): any {
if (key === ReactiveFlags.SKIP) return target[ReactiveFlags.SKIP]
const isReadonly = this._isReadonly, const isReadonly = this._isReadonly,
isShallow = this._isShallow isShallow = this._isShallow
if (key === ReactiveFlags.IS_REACTIVE) { if (key === ReactiveFlags.IS_REACTIVE) {

View File

@ -119,18 +119,25 @@ export class EffectScope {
stop(fromParent?: boolean): void { stop(fromParent?: boolean): void {
if (this._active) { if (this._active) {
this._active = false
let i, l let i, l
for (i = 0, l = this.effects.length; i < l; i++) { for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop() this.effects[i].stop()
} }
this.effects.length = 0
for (i = 0, l = this.cleanups.length; i < l; i++) { for (i = 0, l = this.cleanups.length; i < l; i++) {
this.cleanups[i]() this.cleanups[i]()
} }
this.cleanups.length = 0
if (this.scopes) { if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) { for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].stop(true) this.scopes[i].stop(true)
} }
this.scopes.length = 0
} }
// nested scope, dereference from parent to avoid memory leaks // nested scope, dereference from parent to avoid memory leaks
if (!this.detached && this.parent && !fromParent) { if (!this.detached && this.parent && !fromParent) {
// optimized O(1) removal // optimized O(1) removal
@ -141,7 +148,6 @@ export class EffectScope {
} }
} }
this.parent = undefined this.parent = undefined
this._active = false
} }
} }
} }

View File

@ -213,7 +213,7 @@ export function watch(
const scope = getCurrentScope() const scope = getCurrentScope()
const watchHandle: WatchHandle = () => { const watchHandle: WatchHandle = () => {
effect.stop() effect.stop()
if (scope) { if (scope && scope.active) {
remove(scope.effects, effect) remove(scope.effects, effect)
} }
} }

View File

@ -25,7 +25,6 @@ import {
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { import {
type DebuggerEvent, type DebuggerEvent,
EffectFlags,
ITERATE_KEY, ITERATE_KEY,
type Ref, type Ref,
type ShallowRef, type ShallowRef,
@ -1341,7 +1340,7 @@ describe('api: watch', () => {
await nextTick() await nextTick()
await nextTick() await nextTick()
expect(instance!.scope.effects[0].flags & EffectFlags.ACTIVE).toBeFalsy() expect(instance!.scope.effects.length).toBe(0)
}) })
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => { test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {

View File

@ -87,6 +87,49 @@ describe('renderer: teleport', () => {
`</div>`, `</div>`,
) )
}) })
test('update before mounted with defer', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const show = ref(false)
const foo = ref('foo')
const Header = {
props: { foo: String },
setup(props: any) {
return () => h('div', props.foo)
},
}
const Footer = {
setup() {
foo.value = 'bar'
return () => h('div', 'Footer')
},
}
createDOMApp({
render() {
return show.value
? [
h(
Teleport,
{ to: '#targetId', defer: true },
h(Header, { foo: foo.value }),
),
h(Footer),
h('div', { id: 'targetId' }),
]
: [h('div')]
},
}).mount(root)
expect(root.innerHTML).toMatchInlineSnapshot(`"<div></div>"`)
show.value = true
await nextTick()
expect(root.innerHTML).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end--><div>Footer</div><div id="targetId"><div>bar</div></div>"`,
)
})
}) })
function runSharedTests(deferMode: boolean) { function runSharedTests(deferMode: boolean) {

View File

@ -21,6 +21,7 @@ import {
h, h,
nextTick, nextTick,
onMounted, onMounted,
onServerPrefetch,
openBlock, openBlock,
reactive, reactive,
ref, ref,
@ -518,6 +519,45 @@ describe('SSR hydration', () => {
) )
}) })
test('with data-allow-mismatch component when using onServerPrefetch', async () => {
const Comp = {
template: `
<div>Comp2</div>
`,
}
let foo: any
const App = {
setup() {
const flag = ref(true)
foo = () => {
flag.value = false
}
onServerPrefetch(() => (flag.value = false))
return { flag }
},
components: {
Comp,
},
template: `
<span data-allow-mismatch>
<Comp v-if="flag"></Comp>
</span>
`,
}
// hydrate
const container = document.createElement('div')
container.innerHTML = await renderToString(h(App))
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
'<span data-allow-mismatch=""><div>Comp2</div></span>',
)
foo()
await nextTick()
expect(container.innerHTML).toBe(
'<span data-allow-mismatch=""><!--v-if--></span>',
)
})
test('Teleport unmount (full integration)', async () => { test('Teleport unmount (full integration)', async () => {
const Comp1 = { const Comp1 = {
template: ` template: `
@ -1284,6 +1324,84 @@ describe('SSR hydration', () => {
resolve({}) resolve({})
}) })
//#12362
test('nested async wrapper', async () => {
const Toggle = defineAsyncComponent(
() =>
new Promise(r => {
r(
defineComponent({
setup(_, { slots }) {
const show = ref(false)
onMounted(() => {
nextTick(() => {
show.value = true
})
})
return () =>
withDirectives(
h('div', null, [renderSlot(slots, 'default')]),
[[vShow, show.value]],
)
},
}) as any,
)
}),
)
const Wrapper = defineAsyncComponent(() => {
return new Promise(r => {
r(
defineComponent({
render(this: any) {
return renderSlot(this.$slots, 'default')
},
}) as any,
)
})
})
const count = ref(0)
const fn = vi.fn()
const Child = {
setup() {
onMounted(() => {
fn()
count.value++
})
return () => h('div', count.value)
},
}
const App = {
render() {
return h(Toggle, null, {
default: () =>
h(Wrapper, null, {
default: () =>
h(Wrapper, null, {
default: () => h(Child),
}),
}),
})
},
}
const root = document.createElement('div')
root.innerHTML = await renderToString(h(App))
expect(root.innerHTML).toMatchInlineSnapshot(
`"<div style="display:none;"><!--[--><!--[--><!--[--><div>0</div><!--]--><!--]--><!--]--></div>"`,
)
createSSRApp(App).mount(root)
await nextTick()
await nextTick()
expect(root.innerHTML).toMatchInlineSnapshot(
`"<div style=""><!--[--><!--[--><!--[--><div>1</div><!--]--><!--]--><!--]--></div>"`,
)
expect(fn).toBeCalledTimes(1)
})
test('unmount async wrapper before load (fragment)', async () => { test('unmount async wrapper before load (fragment)', async () => {
let resolve: any let resolve: any
const AsyncComp = defineAsyncComponent( const AsyncComp = defineAsyncComponent(

View File

@ -17,6 +17,7 @@ import {
serializeInner as inner, serializeInner as inner,
nextTick, nextTick,
nodeOps, nodeOps,
onBeforeMount,
onBeforeUnmount, onBeforeUnmount,
onUnmounted, onUnmounted,
openBlock, openBlock,
@ -1199,7 +1200,7 @@ describe('renderer: optimized mode', () => {
createBlock('div', null, [ createBlock('div', null, [
createVNode('div', null, [ createVNode('div', null, [
cache[0] || cache[0] ||
(setBlockTracking(-1), (setBlockTracking(-1, true),
((cache[0] = createVNode('div', null, [ ((cache[0] = createVNode('div', null, [
createVNode(Child), createVNode(Child),
])).cacheIndex = 0), ])).cacheIndex = 0),
@ -1233,4 +1234,64 @@ describe('renderer: optimized mode', () => {
expect(inner(root)).toBe('<!--v-if-->') expect(inner(root)).toBe('<!--v-if-->')
expect(spyUnmounted).toHaveBeenCalledTimes(2) expect(spyUnmounted).toHaveBeenCalledTimes(2)
}) })
// #12371
test('unmount children when the user calls a compiled slot', async () => {
const beforeMountSpy = vi.fn()
const beforeUnmountSpy = vi.fn()
const Child = {
setup() {
onBeforeMount(beforeMountSpy)
onBeforeUnmount(beforeUnmountSpy)
return () => 'child'
},
}
const Wrapper = {
setup(_: any, { slots }: SetupContext) {
return () => (
openBlock(),
createElementBlock('section', null, [
(openBlock(),
createElementBlock('div', { key: 1 }, [
createTextVNode(slots.header!() ? 'foo' : 'bar', 1 /* TEXT */),
renderSlot(slots, 'content'),
])),
])
)
},
}
const show = ref(false)
const app = createApp({
render() {
return show.value
? (openBlock(),
createBlock(Wrapper, null, {
header: withCtx(() => [createVNode({})]),
content: withCtx(() => [createVNode(Child)]),
_: 1,
}))
: createCommentVNode('v-if', true)
},
})
app.mount(root)
expect(inner(root)).toMatchInlineSnapshot(`"<!--v-if-->"`)
expect(beforeMountSpy).toHaveBeenCalledTimes(0)
expect(beforeUnmountSpy).toHaveBeenCalledTimes(0)
show.value = true
await nextTick()
expect(inner(root)).toMatchInlineSnapshot(
`"<section><div>foochild</div></section>"`,
)
expect(beforeMountSpy).toHaveBeenCalledTimes(1)
show.value = false
await nextTick()
expect(inner(root)).toBe('<!--v-if-->')
expect(beforeUnmountSpy).toHaveBeenCalledTimes(1)
})
}) })

View File

@ -1,4 +1,6 @@
import { import {
KeepAlive,
defineAsyncComponent,
defineComponent, defineComponent,
h, h,
nextTick, nextTick,
@ -538,4 +540,68 @@ describe('api: template refs', () => {
'<div><div>[object Object],[object Object]</div><ul><li>2</li><li>3</li></ul></div>', '<div><div>[object Object],[object Object]</div><ul><li>2</li><li>3</li></ul></div>',
) )
}) })
test('with async component which nested in KeepAlive', async () => {
const AsyncComp = defineAsyncComponent(
() =>
new Promise(resolve =>
setTimeout(() =>
resolve(
defineComponent({
setup(_, { expose }) {
expose({
name: 'AsyncComp',
})
return () => h('div')
},
}) as any,
),
),
),
)
const Comp = defineComponent({
setup(_, { expose }) {
expose({
name: 'Comp',
})
return () => h('div')
},
})
const toggle = ref(false)
const instanceRef = ref<any>(null)
const App = {
render: () => {
return h(KeepAlive, () =>
toggle.value
? h(AsyncComp, { ref: instanceRef })
: h(Comp, { ref: instanceRef }),
)
},
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(instanceRef.value.name).toBe('Comp')
// switch to async component
toggle.value = true
await nextTick()
expect(instanceRef.value).toBe(null)
await new Promise(r => setTimeout(r))
expect(instanceRef.value.name).toBe('AsyncComp')
// switch back to normal component
toggle.value = false
await nextTick()
expect(instanceRef.value.name).toBe('Comp')
// switch to async component again
toggle.value = true
await nextTick()
expect(instanceRef.value.name).toBe('AsyncComp')
})
}) })

View File

@ -629,7 +629,7 @@ describe('vnode', () => {
const vnode = const vnode =
(openBlock(), (openBlock(),
createBlock('div', null, [ createBlock('div', null, [
setBlockTracking(-1), setBlockTracking(-1, true),
(vnode1 = (openBlock(), createBlock('div'))), (vnode1 = (openBlock(), createBlock('div'))),
setBlockTracking(1), setBlockTracking(1),
vnode1, vnode1,

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/runtime-core", "name": "@vue/runtime-core",
"version": "3.5.12", "version": "3.5.13",
"description": "@vue/runtime-core", "description": "@vue/runtime-core",
"main": "index.js", "main": "index.js",
"module": "dist/runtime-core.esm-bundler.js", "module": "dist/runtime-core.esm-bundler.js",

View File

@ -149,9 +149,7 @@ export function defineEmits() {
return null as any return null as any
} }
export type ComponentTypeEmits = export type ComponentTypeEmits = ((...args: any[]) => any) | Record<string, any>
| ((...args: any[]) => any)
| Record<string, any[]>
type RecordToUnion<T extends Record<string, any>> = T[keyof T] type RecordToUnion<T extends Record<string, any>> = T[keyof T]

View File

@ -198,8 +198,7 @@ const BaseTransitionImpl: ComponentOptions = {
setTransitionHooks(innerChild, enterHooks) setTransitionHooks(innerChild, enterHooks)
} }
const oldChild = instance.subTree let oldInnerChild = instance.subTree && getInnerChild(instance.subTree)
const oldInnerChild = oldChild && getInnerChild(oldChild)
// handle mode // handle mode
if ( if (
@ -208,7 +207,7 @@ const BaseTransitionImpl: ComponentOptions = {
!isSameVNodeType(innerChild, oldInnerChild) && !isSameVNodeType(innerChild, oldInnerChild) &&
recursiveGetSubtree(instance).type !== Comment recursiveGetSubtree(instance).type !== Comment
) { ) {
const leavingHooks = resolveTransitionHooks( let leavingHooks = resolveTransitionHooks(
oldInnerChild, oldInnerChild,
rawProps, rawProps,
state, state,
@ -228,6 +227,7 @@ const BaseTransitionImpl: ComponentOptions = {
instance.update() instance.update()
} }
delete leavingHooks.afterLeave delete leavingHooks.afterLeave
oldInnerChild = undefined
} }
return emptyPlaceholder(child) return emptyPlaceholder(child)
} else if (mode === 'in-out' && innerChild.type !== Comment) { } else if (mode === 'in-out' && innerChild.type !== Comment) {
@ -238,18 +238,27 @@ const BaseTransitionImpl: ComponentOptions = {
) => { ) => {
const leavingVNodesCache = getLeavingNodesForType( const leavingVNodesCache = getLeavingNodesForType(
state, state,
oldInnerChild, oldInnerChild!,
) )
leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild leavingVNodesCache[String(oldInnerChild!.key)] = oldInnerChild!
// early removal callback // early removal callback
el[leaveCbKey] = () => { el[leaveCbKey] = () => {
earlyRemove() earlyRemove()
el[leaveCbKey] = undefined el[leaveCbKey] = undefined
delete enterHooks.delayedLeave delete enterHooks.delayedLeave
oldInnerChild = undefined
} }
enterHooks.delayedLeave = delayedLeave enterHooks.delayedLeave = () => {
delayedLeave()
delete enterHooks.delayedLeave
oldInnerChild = undefined
} }
} }
} else {
oldInnerChild = undefined
}
} else if (oldInnerChild) {
oldInnerChild = undefined
} }
return child return child

View File

@ -164,11 +164,32 @@ export const TeleportImpl = {
} }
if (isTeleportDeferred(n2.props)) { if (isTeleportDeferred(n2.props)) {
queuePostRenderEffect(mountToTarget, parentSuspense) queuePostRenderEffect(() => {
mountToTarget()
n2.el!.__isMounted = true
}, parentSuspense)
} else { } else {
mountToTarget() mountToTarget()
} }
} else { } else {
if (isTeleportDeferred(n2.props) && !n1.el!.__isMounted) {
queuePostRenderEffect(() => {
TeleportImpl.process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals,
)
delete n1.el!.__isMounted
}, parentSuspense)
return
}
// update content // update content
n2.el = n1.el n2.el = n1.el
n2.targetStart = n1.targetStart n2.targetStart = n1.targetStart

View File

@ -11,7 +11,7 @@ import {
normalizeVNode, normalizeVNode,
} from './vnode' } from './vnode'
import { flushPostFlushCbs } from './scheduler' import { flushPostFlushCbs } from './scheduler'
import type { ComponentInternalInstance } from './component' import type { ComponentInternalInstance, ComponentOptions } from './component'
import { invokeDirectiveHook } from './directives' import { invokeDirectiveHook } from './directives'
import { warn } from './warning' import { warn } from './warning'
import { import {
@ -41,6 +41,7 @@ import {
import type { TeleportImpl, TeleportVNode } from './components/Teleport' import type { TeleportImpl, TeleportVNode } from './components/Teleport'
import { isAsyncWrapper } from './apiAsyncComponent' import { isAsyncWrapper } from './apiAsyncComponent'
import { isReactive } from '@vue/reactivity' import { isReactive } from '@vue/reactivity'
import { updateHOCHostEl } from './componentRenderUtils'
export type RootHydrateFunction = ( export type RootHydrateFunction = (
vnode: VNode<Node, Element>, vnode: VNode<Node, Element>,
@ -307,7 +308,10 @@ export function createHydrationFunctions(
// if component is async, it may get moved / unmounted before its // if component is async, it may get moved / unmounted before its
// inner component is loaded, so we need to give it a placeholder // inner component is loaded, so we need to give it a placeholder
// vnode that matches its adopted DOM. // vnode that matches its adopted DOM.
if (isAsyncWrapper(vnode)) { if (
isAsyncWrapper(vnode) &&
!(vnode.type as ComponentOptions).__asyncResolved
) {
let subTree let subTree
if (isFragmentStart) { if (isFragmentStart) {
subTree = createVNode(Fragment) subTree = createVNode(Fragment)
@ -716,6 +720,11 @@ export function createHydrationFunctions(
getContainerType(container), getContainerType(container),
slotScopeIds, slotScopeIds,
) )
// the component vnode's el should be updated when a mismatch occurs.
if (parentComponent) {
parentComponent.vnode.el = vnode.el
updateHOCHostEl(parentComponent, vnode.el)
}
return next return next
} }

View File

@ -15,7 +15,7 @@ import { isRef, toRaw } from '@vue/reactivity'
import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import type { SchedulerJob } from './scheduler' import type { SchedulerJob } from './scheduler'
import { queuePostRenderEffect } from './renderer' import { queuePostRenderEffect } from './renderer'
import { getComponentPublicInstance } from './component' import { type ComponentOptions, getComponentPublicInstance } from './component'
import { knownTemplateRefs } from './helpers/useTemplateRef' import { knownTemplateRefs } from './helpers/useTemplateRef'
/** /**
@ -42,8 +42,18 @@ export function setRef(
} }
if (isAsyncWrapper(vnode) && !isUnmount) { if (isAsyncWrapper(vnode) && !isUnmount) {
// when mounting async components, nothing needs to be done, // #4999 if an async component already resolved and cached by KeepAlive,
// because the template ref is forwarded to inner component // we need to set the ref to inner component
if (
vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE &&
(vnode.type as ComponentOptions).__asyncResolved &&
vnode.component!.subTree.component
) {
setRef(rawRef, oldRawRef, parentSuspense, vnode.component!.subTree)
}
// otherwise, nothing needs to be done because the template ref
// is forwarded to inner component
return return
} }

View File

@ -301,7 +301,7 @@ export let isBlockTreeEnabled = 1
* *
* ``` js * ``` js
* _cache[1] || ( * _cache[1] || (
* setBlockTracking(-1), * setBlockTracking(-1, true),
* _cache[1] = createVNode(...), * _cache[1] = createVNode(...),
* setBlockTracking(1), * setBlockTracking(1),
* _cache[1] * _cache[1]
@ -310,11 +310,11 @@ export let isBlockTreeEnabled = 1
* *
* @private * @private
*/ */
export function setBlockTracking(value: number): void { export function setBlockTracking(value: number, inVOnce = false): void {
isBlockTreeEnabled += value isBlockTreeEnabled += value
if (value < 0 && currentBlock) { if (value < 0 && currentBlock && inVOnce) {
// mark current block so it doesn't take fast path and skip possible // mark current block so it doesn't take fast path and skip possible
// nested components duriung unmount // nested components during unmount
currentBlock.hasOnce = true currentBlock.hasOnce = true
} }
} }

View File

@ -396,6 +396,38 @@ describe('defineCustomElement', () => {
expect(e.value).toBe('hi') expect(e.value).toBe('hi')
}) })
// #12214
test('Boolean prop with default true', async () => {
const E = defineCustomElement({
props: {
foo: {
type: Boolean,
default: true,
},
},
render() {
return String(this.foo)
},
})
customElements.define('my-el-default-true', E)
container.innerHTML = `<my-el-default-true></my-el-default-true>`
const e = container.childNodes[0] as HTMLElement & { foo: any },
shadowRoot = e.shadowRoot as ShadowRoot
expect(shadowRoot.innerHTML).toBe('true')
e.foo = undefined
await nextTick()
expect(shadowRoot.innerHTML).toBe('true')
e.foo = false
await nextTick()
expect(shadowRoot.innerHTML).toBe('false')
e.foo = null
await nextTick()
expect(shadowRoot.innerHTML).toBe('null')
e.foo = ''
await nextTick()
expect(shadowRoot.innerHTML).toBe('true')
})
test('support direct setup function syntax with extra options', () => { test('support direct setup function syntax with extra options', () => {
const E = defineCustomElement( const E = defineCustomElement(
props => { props => {

View File

@ -7,6 +7,7 @@ import {
defineCustomElement, defineCustomElement,
h, h,
nextTick, nextTick,
onMounted,
reactive, reactive,
ref, ref,
render, render,
@ -384,6 +385,44 @@ describe('useCssVars', () => {
} }
}) })
test('with delay mount child', async () => {
const state = reactive({ color: 'red' })
const value = ref(false)
const root = document.createElement('div')
const Child = {
setup() {
onMounted(() => {
const childEl = root.children[0]
expect(getComputedStyle(childEl!).getPropertyValue(`--color`)).toBe(
`red`,
)
})
return () => h('div', { id: 'childId' })
},
}
const App = {
setup() {
useCssVars(() => state)
return () => (value.value ? h(Child) : [h('span')])
},
}
render(h(App), root)
await nextTick()
// css vars use with fallback tree
for (const c of [].slice.call(root.children as any)) {
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
}
// mount child
value.value = true
await nextTick()
for (const c of [].slice.call(root.children as any)) {
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
}
})
// #8826 // #8826
test('with custom element', async () => { test('with custom element', async () => {
const state = reactive({ color: 'red' }) const state = reactive({ color: 'red' })
@ -405,4 +444,25 @@ describe('useCssVars', () => {
`<css-vars-ce style="--color: red;"></css-vars-ce>`, `<css-vars-ce style="--color: red;"></css-vars-ce>`,
) )
}) })
test('should set vars before child component onMount hook', () => {
const state = reactive({ color: 'red' })
const root = document.createElement('div')
let colorInOnMount
const App = {
setup() {
useCssVars(() => state)
onMounted(() => {
colorInOnMount = (
root.children[0] as HTMLElement
).style.getPropertyValue(`--color`)
})
return () => h('div')
},
}
render(h(App), root)
expect(colorInOnMount).toBe(`red`)
})
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/runtime-dom", "name": "@vue/runtime-dom",
"version": "3.5.12", "version": "3.5.13",
"description": "@vue/runtime-dom", "description": "@vue/runtime-dom",
"main": "index.js", "main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js", "module": "dist/runtime-dom.esm-bundler.js",

View File

@ -505,6 +505,8 @@ export class VueElement
} }
// reflect // reflect
if (shouldReflect) { if (shouldReflect) {
const ob = this._ob
ob && ob.disconnect()
if (val === true) { if (val === true) {
this.setAttribute(hyphenate(key), '') this.setAttribute(hyphenate(key), '')
} else if (typeof val === 'string' || typeof val === 'number') { } else if (typeof val === 'string' || typeof val === 'number') {
@ -512,6 +514,7 @@ export class VueElement
} else if (!val) { } else if (!val) {
this.removeAttribute(hyphenate(key)) this.removeAttribute(hyphenate(key))
} }
ob && ob.observe(this, { attributes: true })
} }
} }
} }

View File

@ -181,7 +181,13 @@ export function resolveTransitionProps(
onAppearCancelled = onEnterCancelled, onAppearCancelled = onEnterCancelled,
} = baseProps } = baseProps
const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => { const finishEnter = (
el: Element & { _enterCancelled?: boolean },
isAppear: boolean,
done?: () => void,
isCancelled?: boolean,
) => {
el._enterCancelled = isCancelled
removeTransitionClass(el, isAppear ? appearToClass : enterToClass) removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass) removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
done && done() done && done()
@ -240,7 +246,10 @@ export function resolveTransitionProps(
}, },
onEnter: makeEnterHook(false), onEnter: makeEnterHook(false),
onAppear: makeEnterHook(true), onAppear: makeEnterHook(true),
onLeave(el: Element & { _isLeaving?: boolean }, done) { onLeave(
el: Element & { _isLeaving?: boolean; _enterCancelled?: boolean },
done,
) {
el._isLeaving = true el._isLeaving = true
const resolve = () => finishLeave(el, done) const resolve = () => finishLeave(el, done)
addTransitionClass(el, leaveFromClass) addTransitionClass(el, leaveFromClass)
@ -249,9 +258,14 @@ export function resolveTransitionProps(
} }
// add *-leave-active class before reflow so in the case of a cancelled enter transition // add *-leave-active class before reflow so in the case of a cancelled enter transition
// the css will not get the final state (#10677) // the css will not get the final state (#10677)
addTransitionClass(el, leaveActiveClass) if (!el._enterCancelled) {
// force reflow so *-leave-from classes immediately take effect (#2593) // force reflow so *-leave-from classes immediately take effect (#2593)
forceReflow() forceReflow()
addTransitionClass(el, leaveActiveClass)
} else {
addTransitionClass(el, leaveActiveClass)
forceReflow()
}
nextFrame(() => { nextFrame(() => {
if (!el._isLeaving) { if (!el._isLeaving) {
// cancelled // cancelled
@ -269,11 +283,11 @@ export function resolveTransitionProps(
callHook(onLeave, [el, resolve]) callHook(onLeave, [el, resolve])
}, },
onEnterCancelled(el) { onEnterCancelled(el) {
finishEnter(el, false) finishEnter(el, false, undefined, true)
callHook(onEnterCancelled, [el]) callHook(onEnterCancelled, [el])
}, },
onAppearCancelled(el) { onAppearCancelled(el) {
finishEnter(el, true) finishEnter(el, true, undefined, true)
callHook(onAppearCancelled, [el]) callHook(onAppearCancelled, [el])
}, },
onLeaveCancelled(el) { onLeaveCancelled(el) {

View File

@ -3,13 +3,14 @@ import {
Static, Static,
type VNode, type VNode,
getCurrentInstance, getCurrentInstance,
onBeforeMount, onBeforeUpdate,
onMounted, onMounted,
onUnmounted, onUnmounted,
queuePostFlushCb,
warn, warn,
watchPostEffect, watch,
} from '@vue/runtime-core' } from '@vue/runtime-core'
import { ShapeFlags } from '@vue/shared' import { NOOP, ShapeFlags } from '@vue/shared'
export const CSS_VAR_TEXT: unique symbol = Symbol(__DEV__ ? 'CSS_VAR_TEXT' : '') export const CSS_VAR_TEXT: unique symbol = Symbol(__DEV__ ? 'CSS_VAR_TEXT' : '')
/** /**
@ -48,11 +49,15 @@ export function useCssVars(getter: (ctx: any) => Record<string, string>): void {
updateTeleports(vars) updateTeleports(vars)
} }
onBeforeMount(() => { // handle cases where child component root is affected
watchPostEffect(setVars) // and triggers reflow in onMounted
onBeforeUpdate(() => {
queuePostFlushCb(setVars)
}) })
onMounted(() => { onMounted(() => {
// run setVars synchronously here, but run as post-effect on changes
watch(setVars, NOOP, { flush: 'post' })
const ob = new MutationObserver(setVars) const ob = new MutationObserver(setVars)
ob.observe(instance.subTree.el!.parentNode, { childList: true }) ob.observe(instance.subTree.el!.parentNode, { childList: true })
onUnmounted(() => ob.disconnect()) onUnmounted(() => ob.disconnect())

View File

@ -1,6 +1,10 @@
import { bench, describe } from 'vitest' import { bench, describe } from 'vitest'
import { createBuffer } from '../src/render' import { createBuffer as _createBuffer } from '../src/render'
// move to local const to avoid import access overhead
// https://github.com/vitest-dev/vitest/issues/6903
const createBuffer = _createBuffer
describe('createBuffer', () => { describe('createBuffer', () => {
let stringBuffer = createBuffer() let stringBuffer = createBuffer()

View File

@ -1,7 +1,11 @@
import { bench, describe } from 'vitest' import { bench, describe } from 'vitest'
import { type SSRBuffer, createBuffer } from '../src/render' import { type SSRBuffer, createBuffer } from '../src/render'
import { unrollBuffer } from '../src/renderToString' import { unrollBuffer as _unrollBuffer } from '../src/renderToString'
// move to local const to avoid import access overhead
// https://github.com/vitest-dev/vitest/issues/6903
const unrollBuffer = _unrollBuffer
function createSyncBuffer(levels: number, itemsPerLevel: number): SSRBuffer { function createSyncBuffer(levels: number, itemsPerLevel: number): SSRBuffer {
const buffer = createBuffer() const buffer = createBuffer()

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/server-renderer", "name": "@vue/server-renderer",
"version": "3.5.12", "version": "3.5.13",
"description": "@vue/server-renderer", "description": "@vue/server-renderer",
"main": "index.js", "main": "index.js",
"module": "dist/server-renderer.esm-bundler.js", "module": "dist/server-renderer.esm-bundler.js",

View File

@ -153,10 +153,10 @@ describe('normalizeStyle', () => {
}) })
describe('stringifyStyle', () => { describe('stringifyStyle', () => {
test('should return empty string for undefined or string styles', () => { test('should return empty string for undefined', () => {
expect(stringifyStyle(undefined)).toBe('') expect(stringifyStyle(undefined)).toBe('')
expect(stringifyStyle('')).toBe('') expect(stringifyStyle('')).toBe('')
expect(stringifyStyle('color: blue;')).toBe('') expect(stringifyStyle('color: blue;')).toBe('color: blue;')
}) })
test('should return valid CSS string for normalized style object', () => { test('should return valid CSS string for normalized style object', () => {

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/shared", "name": "@vue/shared",
"version": "3.5.12", "version": "3.5.13",
"description": "internal utils shared across @vue packages", "description": "internal utils shared across @vue packages",
"main": "index.js", "main": "index.js",
"module": "dist/shared.esm-bundler.js", "module": "dist/shared.esm-bundler.js",

View File

@ -45,10 +45,10 @@ export function parseStringStyle(cssText: string): NormalizedStyle {
export function stringifyStyle( export function stringifyStyle(
styles: NormalizedStyle | string | undefined, styles: NormalizedStyle | string | undefined,
): string { ): string {
if (!styles) return ''
if (isString(styles)) return styles
let ret = '' let ret = ''
if (!styles || isString(styles)) {
return ret
}
for (const key in styles) { for (const key in styles) {
const value = styles[key] const value = styles[key]
if (isString(value) || typeof value === 'number') { if (isString(value) || typeof value === 'number') {

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compat", "name": "@vue/compat",
"version": "3.5.12", "version": "3.5.13",
"description": "Vue 3 compatibility build for Vue 2", "description": "Vue 3 compatibility build for Vue 2",
"main": "index.js", "main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js", "module": "dist/vue.runtime.esm-bundler.js",

View File

@ -3,7 +3,7 @@ import path from 'node:path'
import { Transition, createApp, h, nextTick, ref } from 'vue' import { Transition, createApp, h, nextTick, ref } from 'vue'
describe('e2e: Transition', () => { describe('e2e: Transition', () => {
const { page, html, classList, isVisible, timeout, nextFrame, click } = const { page, html, classList, style, isVisible, timeout, nextFrame, click } =
setupPuppeteer() setupPuppeteer()
const baseUrl = `file://${path.resolve(__dirname, './transition.html')}` const baseUrl = `file://${path.resolve(__dirname, './transition.html')}`
@ -2986,6 +2986,55 @@ describe('e2e: Transition', () => {
) )
}) })
test('reflow after *-leave-from before *-leave-active', async () => {
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
template: `
<div id="container">
<transition name="test-reflow">
<div v-if="toggle" class="test-reflow">content</div>
</transition>
</div>
<button id="toggleBtn" @click="click">button</button>
`,
setup: () => {
const toggle = ref(false)
const click = () => (toggle.value = !toggle.value)
return {
toggle,
click,
}
},
}).mount('#app')
})
// if transition starts while there's v-leave-active added along with v-leave-from, its bad, it has to start when it doesnt have the v-leave-from
// enter
await classWhenTransitionStart()
await transitionFinish()
// leave
expect(await classWhenTransitionStart()).toStrictEqual([
'test-reflow',
'test-reflow-leave-from',
'test-reflow-leave-active',
])
expect(await style('.test-reflow', 'opacity')).toStrictEqual('0.9')
await nextFrame()
expect(await classList('.test-reflow')).toStrictEqual([
'test-reflow',
'test-reflow-leave-active',
'test-reflow-leave-to',
])
await transitionFinish()
expect(await html('#container')).toBe('<!--v-if-->')
})
test('warn when used on multiple elements', async () => { test('warn when used on multiple elements', async () => {
createApp({ createApp({
render() { render() {
@ -3121,4 +3170,124 @@ describe('e2e: Transition', () => {
}, },
E2E_TIMEOUT, E2E_TIMEOUT,
) )
// https://github.com/vuejs/core/issues/12181#issuecomment-2414380955
describe('not leaking', async () => {
test('switching VNodes', async () => {
const client = await page().createCDPSession()
await page().evaluate(async () => {
const { createApp, ref, nextTick } = (window as any).Vue
const empty = ref(true)
createApp({
components: {
Child: {
setup: () => {
// Big arrays kick GC earlier
const test = ref([...Array(30_000_000)].map((_, i) => ({ i })))
// TODO: Use a diferent TypeScript env for testing
// @ts-expect-error - Custom property and same lib as runtime is used
window.__REF__ = new WeakRef(test)
return { test }
},
template: `
<p>{{ test.length }}</p>
`,
},
Empty: {
template: '<div></div>',
},
},
template: `
<transition>
<component :is="empty ? 'Empty' : 'Child'" />
</transition>
`,
setup() {
return { empty }
},
}).mount('#app')
await nextTick()
empty.value = false
await nextTick()
empty.value = true
await nextTick()
})
const isCollected = async () =>
// @ts-expect-error - Custom property
await page().evaluate(() => window.__REF__.deref() === undefined)
while ((await isCollected()) === false) {
await client.send('HeapProfiler.collectGarbage')
}
expect(await isCollected()).toBe(true)
})
// https://github.com/vuejs/core/issues/12181#issue-2588232334
test('switching deep vnodes edge case', async () => {
const client = await page().createCDPSession()
await page().evaluate(async () => {
const { createApp, ref, nextTick } = (window as any).Vue
const shown = ref(false)
createApp({
components: {
Child: {
setup: () => {
// Big arrays kick GC earlier
const test = ref([...Array(30_000_000)].map((_, i) => ({ i })))
// TODO: Use a diferent TypeScript env for testing
// @ts-expect-error - Custom property and same lib as runtime is used
window.__REF__ = new WeakRef(test)
return { test }
},
template: `
<p>{{ test.length }}</p>
`,
},
Wrapper: {
template: `
<transition>
<div v-if="true">
<slot />
</div>
</transition>
`,
},
},
template: `
<button id="toggleBtn" @click="shown = !shown">{{ shown ? 'Hide' : 'Show' }}</button>
<Wrapper>
<Child v-if="shown" />
<div v-else></div>
</Wrapper>
`,
setup() {
return { shown }
},
}).mount('#app')
await nextTick()
shown.value = true
await nextTick()
shown.value = false
await nextTick()
})
const isCollected = async () =>
// @ts-expect-error - Custom property
await page().evaluate(() => window.__REF__.deref() === undefined)
while ((await isCollected()) === false) {
await client.send('HeapProfiler.collectGarbage')
}
expect(await isCollected()).toBe(true)
})
})
}) })

View File

@ -39,6 +39,7 @@ interface PuppeteerUtils {
value(selector: string): Promise<string> value(selector: string): Promise<string>
html(selector: string): Promise<string> html(selector: string): Promise<string>
classList(selector: string): Promise<string[]> classList(selector: string): Promise<string[]>
style(selector: string, property: keyof CSSStyleDeclaration): Promise<any>
children(selector: string): Promise<any[]> children(selector: string): Promise<any[]>
isVisible(selector: string): Promise<boolean> isVisible(selector: string): Promise<boolean>
isChecked(selector: string): Promise<boolean> isChecked(selector: string): Promise<boolean>
@ -120,6 +121,19 @@ export function setupPuppeteer(args?: string[]): PuppeteerUtils {
return page.$eval(selector, (node: any) => [...node.children]) return page.$eval(selector, (node: any) => [...node.children])
} }
async function style(
selector: string,
property: keyof CSSStyleDeclaration,
): Promise<any> {
return await page.$eval(
selector,
(node, property) => {
return window.getComputedStyle(node)[property]
},
property,
)
}
async function isVisible(selector: string): Promise<boolean> { async function isVisible(selector: string): Promise<boolean> {
const display = await page.$eval(selector, node => { const display = await page.$eval(selector, node => {
return window.getComputedStyle(node).display return window.getComputedStyle(node).display
@ -195,6 +209,7 @@ export function setupPuppeteer(args?: string[]): PuppeteerUtils {
value, value,
html, html,
classList, classList,
style,
children, children,
isVisible, isVisible,
isChecked, isChecked,

View File

@ -16,11 +16,21 @@
.test-appear, .test-appear,
.test-enter, .test-enter,
.test-leave-active, .test-leave-active,
.test-reflow-enter,
.test-reflow-leave-to,
.hello, .hello,
.bye.active, .bye.active,
.changed-enter { .changed-enter {
opacity: 0; opacity: 0;
} }
.test-reflow-leave-active,
.test-reflow-enter-active {
-webkit-transition: opacity 50ms ease;
transition: opacity 50ms ease;
}
.test-reflow-leave-from {
opacity: 0.9;
}
.test-anim-enter-active { .test-anim-enter-active {
animation: test-enter 50ms; animation: test-enter 50ms;
-webkit-animation: test-enter 50ms; -webkit-animation: test-enter 50ms;

View File

@ -1,6 +1,6 @@
{ {
"name": "vue", "name": "vue",
"version": "3.5.12", "version": "3.5.13",
"description": "The progressive JavaScript framework for building modern web UI.", "description": "The progressive JavaScript framework for building modern web UI.",
"main": "index.js", "main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js", "module": "dist/vue.runtime.esm-bundler.js",

View File

@ -358,8 +358,8 @@ importers:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0(postcss@8.4.48) version: 6.0.0(postcss@8.4.48)
postcss-selector-parser: postcss-selector-parser:
specifier: ^6.1.2 specifier: ^7.0.0
version: 6.1.2 version: 7.0.0
pug: pug:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
@ -3027,6 +3027,10 @@ packages:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'} engines: {node: '>=4'}
postcss-selector-parser@7.0.0:
resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0: postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@ -6214,6 +6218,11 @@ snapshots:
cssesc: 3.0.0 cssesc: 3.0.0
util-deprecate: 1.0.2 util-deprecate: 1.0.2
postcss-selector-parser@7.0.0:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {} postcss-value-parser@4.2.0: {}
postcss@8.4.48: postcss@8.4.48:

View File

@ -46,6 +46,12 @@ const pkg = require(resolve(`package.json`))
const packageOptions = pkg.buildOptions || {} const packageOptions = pkg.buildOptions || {}
const name = packageOptions.filename || path.basename(packageDir) const name = packageOptions.filename || path.basename(packageDir)
const banner = `/**
* ${pkg.name} v${masterVersion}
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/`
const [enumPlugin, enumDefines] = inlineEnums() const [enumPlugin, enumDefines] = inlineEnums()
/** @type {Record<PackageFormat, OutputOptions>} */ /** @type {Record<PackageFormat, OutputOptions>} */
@ -138,11 +144,7 @@ function createConfig(format, output, plugins = []) {
(isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) && (isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) &&
!packageOptions.enableNonBrowserBranches !packageOptions.enableNonBrowserBranches
output.banner = `/** output.banner = banner
* ${pkg.name} v${masterVersion}
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/`
output.exports = isCompatPackage ? 'auto' : 'named' output.exports = isCompatPackage ? 'auto' : 'named'
if (isCJSBuild) { if (isCJSBuild) {
@ -375,24 +377,21 @@ function createMinifiedConfig(/** @type {PackageFormat} */ format) {
{ {
name: 'swc-minify', name: 'swc-minify',
async renderChunk( async renderChunk(contents, _, { format }) {
contents, const { code } = await minifySwc(contents, {
_,
{ format, sourcemap, sourcemapExcludeSources },
) {
const { code, map } = await minifySwc(contents, {
module: format === 'es', module: format === 'es',
format: {
comments: false,
},
compress: { compress: {
ecma: 2016, ecma: 2016,
pure_getters: true, pure_getters: true,
}, },
safari10: true, safari10: true,
mangle: true, mangle: true,
sourceMap: !!sourcemap,
inlineSourcesContent: !sourcemapExcludeSources,
}) })
return { code, map: map || null } return { code: banner + code, map: null }
}, },
}, },
], ],