mirror of https://github.com/vuejs/core.git
Merge tag 'v3.5.13'
This commit is contained in:
commit
f6ce3f9d43
|
@ -14,5 +14,6 @@
|
|||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
https://vuejs.org/funding.json
|
33
CHANGELOG.md
33
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"private": true,
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"packageManager": "pnpm@9.12.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
@ -22,7 +22,10 @@
|
|||
"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-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",
|
||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
|
||||
"dev-esm": "node scripts/dev.js -if esm-bundler-runtime",
|
||||
|
|
|
@ -306,6 +306,14 @@ describe('defineEmits w/ type declaration', () => {
|
|||
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', () => {
|
||||
const emit = defineEmits<{
|
||||
foo: [id: string]
|
||||
|
|
|
@ -48,6 +48,7 @@ function resetVueVersion() {
|
|||
|
||||
async function copyLink(e: MouseEvent) {
|
||||
if (e.metaKey) {
|
||||
resetVueVersion()
|
||||
// hidden logic for going to local debug from play.vuejs.org
|
||||
window.location.href = 'http://localhost:5173/' + window.location.hash
|
||||
return
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
// 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`] = `
|
||||
"import { resolveComponent as _resolveComponent, createVNode as _createVNode, withMemo as _withMemo, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ return function render(_ctx, _cache) {
|
|||
const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode } = _Vue
|
||||
|
||||
return _cache[0] || (
|
||||
_setBlockTracking(-1),
|
||||
_setBlockTracking(-1, true),
|
||||
(_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
|
||||
_setBlockTracking(1),
|
||||
_cache[0]
|
||||
|
@ -28,7 +28,7 @@ return function render(_ctx, _cache) {
|
|||
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
_cache[0] || (
|
||||
_setBlockTracking(-1),
|
||||
_setBlockTracking(-1, true),
|
||||
(_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
|
||||
_setBlockTracking(1),
|
||||
_cache[0]
|
||||
|
@ -47,7 +47,7 @@ return function render(_ctx, _cache) {
|
|||
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
_cache[0] || (
|
||||
_setBlockTracking(-1),
|
||||
_setBlockTracking(-1, true),
|
||||
(_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
|
||||
_setBlockTracking(1),
|
||||
_cache[0]
|
||||
|
@ -66,7 +66,7 @@ return function render(_ctx, _cache) {
|
|||
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
_cache[0] || (
|
||||
_setBlockTracking(-1),
|
||||
_setBlockTracking(-1, true),
|
||||
(_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0,
|
||||
_setBlockTracking(1),
|
||||
_cache[0]
|
||||
|
@ -85,7 +85,7 @@ return function render(_ctx, _cache) {
|
|||
|
||||
return (_openBlock(), _createElementBlock("div", null, [
|
||||
_cache[0] || (
|
||||
_setBlockTracking(-1),
|
||||
_setBlockTracking(-1, true),
|
||||
(_cache[0] = _createElementVNode("div")).cacheIndex = 0,
|
||||
_setBlockTracking(1),
|
||||
_cache[0]
|
||||
|
|
|
@ -53,4 +53,12 @@ describe('compiler: v-memo transform', () => {
|
|||
),
|
||||
).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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-core",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "@vue/compiler-core",
|
||||
"main": "index.js",
|
||||
"module": "dist/compiler-core.esm-bundler.js",
|
||||
|
|
|
@ -425,6 +425,7 @@ export interface CacheExpression extends Node {
|
|||
index: number
|
||||
value: JSChildNode
|
||||
needPauseTracking: boolean
|
||||
inVOnce: boolean
|
||||
needArraySpread: boolean
|
||||
}
|
||||
|
||||
|
@ -781,12 +782,14 @@ export function createCacheExpression(
|
|||
index: number,
|
||||
value: JSChildNode,
|
||||
needPauseTracking: boolean = false,
|
||||
inVOnce: boolean = false,
|
||||
): CacheExpression {
|
||||
return {
|
||||
type: NodeTypes.JS_CACHE_EXPRESSION,
|
||||
index,
|
||||
value,
|
||||
needPauseTracking: needPauseTracking,
|
||||
inVOnce,
|
||||
needArraySpread: false,
|
||||
loc: locStub,
|
||||
}
|
||||
|
|
|
@ -1034,7 +1034,9 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
|
|||
push(`_cache[${node.index}] || (`)
|
||||
if (needPauseTracking) {
|
||||
indent()
|
||||
push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
|
||||
push(`${helper(SET_BLOCK_TRACKING)}(-1`)
|
||||
if (node.inVOnce) push(`, true`)
|
||||
push(`),`)
|
||||
newline()
|
||||
push(`(`)
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ export interface TransformContext
|
|||
addIdentifiers(exp: ExpressionNode | string): void
|
||||
removeIdentifiers(exp: ExpressionNode | string): void
|
||||
hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode
|
||||
cache(exp: JSChildNode, isVNode?: boolean): CacheExpression
|
||||
cache(exp: JSChildNode, isVNode?: boolean, inVOnce?: boolean): CacheExpression
|
||||
constantCache: WeakMap<TemplateChildNode, ConstantTypes>
|
||||
|
||||
// 2.x Compat only
|
||||
|
@ -297,11 +297,12 @@ export function createTransformContext(
|
|||
identifier.hoisted = exp
|
||||
return identifier
|
||||
},
|
||||
cache(exp, isVNode = false) {
|
||||
cache(exp, isVNode = false, inVOnce = false) {
|
||||
const cacheExp = createCacheExpression(
|
||||
context.cached.length,
|
||||
exp,
|
||||
isVNode,
|
||||
inVOnce,
|
||||
)
|
||||
context.cached.push(cacheExp)
|
||||
return cacheExp
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
isStaticPropertyKey,
|
||||
walkIdentifiers,
|
||||
} from '../babelUtils'
|
||||
import { advancePositionWithClone, isSimpleIdentifier } from '../utils'
|
||||
import { advancePositionWithClone, findDir, isSimpleIdentifier } from '../utils'
|
||||
import {
|
||||
genPropsAccessExp,
|
||||
hasOwn,
|
||||
|
@ -55,6 +55,7 @@ export const transformExpression: NodeTransform = (node, context) => {
|
|||
)
|
||||
} else if (node.type === NodeTypes.ELEMENT) {
|
||||
// handle directives on element
|
||||
const memo = findDir(node, 'memo')
|
||||
for (let i = 0; i < node.props.length; i++) {
|
||||
const dir = node.props[i]
|
||||
// do not process for v-on & v-for since they are special handled
|
||||
|
@ -66,7 +67,14 @@ export const transformExpression: NodeTransform = (node, context) => {
|
|||
if (
|
||||
exp &&
|
||||
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(
|
||||
exp,
|
||||
|
|
|
@ -63,17 +63,27 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
|
|||
const isTemplate = isTemplateNode(node)
|
||||
const memo = findDir(node, 'memo')
|
||||
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
|
||||
transformBindShorthand(keyProp, context)
|
||||
}
|
||||
const keyExp =
|
||||
let keyExp =
|
||||
keyProp &&
|
||||
(keyProp.type === NodeTypes.ATTRIBUTE
|
||||
? keyProp.value
|
||||
? createSimpleExpression(keyProp.value.content, true)
|
||||
: undefined
|
||||
: keyProp.exp)
|
||||
|
||||
if (memo && keyExp && isDirKey) {
|
||||
if (!__BROWSER__) {
|
||||
keyProp.exp = keyExp = processExpression(
|
||||
keyExp as SimpleExpressionNode,
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
const keyProperty =
|
||||
keyProp && keyExp ? createObjectProperty(`key`, keyExp) : null
|
||||
|
||||
|
|
|
@ -17,7 +17,11 @@ export const transformOnce: NodeTransform = (node, context) => {
|
|||
context.inVOnce = false
|
||||
const cur = context.currentNode as ElementNode | IfNode | ForNode
|
||||
if (cur.codegenNode) {
|
||||
cur.codegenNode = context.cache(cur.codegenNode, true /* isVNode */)
|
||||
cur.codegenNode = context.cache(
|
||||
cur.codegenNode,
|
||||
true /* isVNode */,
|
||||
true /* inVOnce */,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`] = `
|
||||
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||
|
||||
|
|
|
@ -162,6 +162,27 @@ describe('stringify static html', () => {
|
|||
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', () => {
|
||||
const { ast, code } = compileWithStringify(
|
||||
`<div><div>${repeat(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-dom",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "@vue/compiler-dom",
|
||||
"main": "index.js",
|
||||
"module": "dist/compiler-dom.esm-bundler.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-sfc",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "@vue/compiler-sfc",
|
||||
"main": "dist/compiler-sfc.cjs.js",
|
||||
"module": "dist/compiler-sfc.esm-browser.js",
|
||||
|
@ -61,7 +61,7 @@
|
|||
"merge-source-map": "^1.1.0",
|
||||
"minimatch": "~9.0.5",
|
||||
"postcss-modules": "^6.0.0",
|
||||
"postcss-selector-parser": "^6.1.2",
|
||||
"postcss-selector-parser": "^7.0.0",
|
||||
"pug": "^3.0.3",
|
||||
"sass": "^1.80.6"
|
||||
}
|
||||
|
|
|
@ -189,8 +189,7 @@ function rewriteSelector(
|
|||
// global: replace with inner selector and do not inject [id].
|
||||
// ::v-global(.foo) -> .foo
|
||||
if (value === ':global' || value === '::v-global') {
|
||||
selectorRoot.insertAfter(selector, n.nodes[0])
|
||||
selectorRoot.removeChild(selector)
|
||||
selector.replaceWith(n.nodes[0])
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
expect(getCompiledString(`<div v-bind="x" v-xxx />`))
|
||||
.toMatchInlineSnapshot(`
|
||||
|
|
|
@ -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(
|
||||
compileWithWrapper(
|
||||
`<select multiple v-model="model"><option value="1" selected></option><option value="2"></option></select>`,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compiler-ssr",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "@vue/compiler-ssr",
|
||||
"main": "dist/compiler-ssr.cjs.js",
|
||||
"types": "dist/compiler-ssr.d.ts",
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
createSequenceExpression,
|
||||
createSimpleExpression,
|
||||
createTemplateLiteral,
|
||||
findDir,
|
||||
hasDynamicKeyVBind,
|
||||
isStaticArgOf,
|
||||
isStaticExp,
|
||||
|
@ -164,24 +165,28 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
|
|||
]
|
||||
}
|
||||
} else if (directives.length && !node.children.length) {
|
||||
const tempId = `_temp${context.temps++}`
|
||||
propsExp.arguments = [
|
||||
createAssignmentExpression(
|
||||
createSimpleExpression(tempId, false),
|
||||
mergedProps,
|
||||
),
|
||||
]
|
||||
rawChildrenMap.set(
|
||||
node,
|
||||
createConditionalExpression(
|
||||
createSimpleExpression(`"textContent" in ${tempId}`, false),
|
||||
createCallExpression(context.helper(SSR_INTERPOLATE), [
|
||||
createSimpleExpression(`${tempId}.textContent`, false),
|
||||
]),
|
||||
createSimpleExpression(`${tempId}.innerHTML ?? ''`, false),
|
||||
false,
|
||||
),
|
||||
)
|
||||
// v-text directive has higher priority than the merged props
|
||||
const vText = findDir(node, 'text')
|
||||
if (!vText) {
|
||||
const tempId = `_temp${context.temps++}`
|
||||
propsExp.arguments = [
|
||||
createAssignmentExpression(
|
||||
createSimpleExpression(tempId, false),
|
||||
mergedProps,
|
||||
),
|
||||
]
|
||||
rawChildrenMap.set(
|
||||
node,
|
||||
createConditionalExpression(
|
||||
createSimpleExpression(`"textContent" in ${tempId}`, false),
|
||||
createCallExpression(context.helper(SSR_INTERPOLATE), [
|
||||
createSimpleExpression(`${tempId}.textContent`, false),
|
||||
]),
|
||||
createSimpleExpression(`${tempId}.innerHTML ?? ''`, false),
|
||||
false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (needTagForRuntime) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
type ExpressionNode,
|
||||
NodeTypes,
|
||||
type PlainElementNode,
|
||||
type TemplateChildNode,
|
||||
createCallExpression,
|
||||
createConditionalExpression,
|
||||
createDOMCompilerError,
|
||||
|
@ -162,11 +163,18 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
|
|||
checkDuplicatedValue()
|
||||
node.children = [createInterpolation(model, model.loc)]
|
||||
} else if (node.tag === 'select') {
|
||||
node.children.forEach(child => {
|
||||
if (child.type === NodeTypes.ELEMENT) {
|
||||
processOption(child as PlainElementNode)
|
||||
}
|
||||
})
|
||||
const processChildren = (children: TemplateChildNode[]) => {
|
||||
children.forEach(child => {
|
||||
if (child.type === NodeTypes.ELEMENT) {
|
||||
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 {
|
||||
context.onError(
|
||||
createDOMCompilerError(
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
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', () => {
|
||||
bench('create computed', () => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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', () => {
|
||||
{
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
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) {
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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>) {
|
||||
const map = new Map()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { bench } from 'vitest'
|
||||
import { reactive } from '../src'
|
||||
import { reactive } from '../dist/reactivity.esm-browser.prod'
|
||||
|
||||
bench('create reactive obj', () => {
|
||||
reactive({ a: 1 })
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { bench, describe } from 'vitest'
|
||||
import { ref } from '../src/index'
|
||||
import { ref } from '../dist/reactivity.esm-browser.prod'
|
||||
|
||||
describe('ref', () => {
|
||||
bench('create ref', () => {
|
||||
|
|
|
@ -176,7 +176,7 @@ describe('reactivity/effect/scope', () => {
|
|||
|
||||
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
|
||||
expect(dummy).toBe(0)
|
||||
|
@ -322,4 +322,44 @@ describe('reactivity/effect/scope', () => {
|
|||
scope.resume()
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/reactivity",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "@vue/reactivity",
|
||||
"main": "index.js",
|
||||
"module": "dist/reactivity.esm-bundler.js",
|
||||
|
|
|
@ -53,6 +53,8 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
|
|||
) {}
|
||||
|
||||
get(target: Target, key: string | symbol, receiver: object): any {
|
||||
if (key === ReactiveFlags.SKIP) return target[ReactiveFlags.SKIP]
|
||||
|
||||
const isReadonly = this._isReadonly,
|
||||
isShallow = this._isShallow
|
||||
if (key === ReactiveFlags.IS_REACTIVE) {
|
||||
|
|
|
@ -119,18 +119,25 @@ export class EffectScope {
|
|||
|
||||
stop(fromParent?: boolean): void {
|
||||
if (this._active) {
|
||||
this._active = false
|
||||
let i, l
|
||||
for (i = 0, l = this.effects.length; i < l; i++) {
|
||||
this.effects[i].stop()
|
||||
}
|
||||
this.effects.length = 0
|
||||
|
||||
for (i = 0, l = this.cleanups.length; i < l; i++) {
|
||||
this.cleanups[i]()
|
||||
}
|
||||
this.cleanups.length = 0
|
||||
|
||||
if (this.scopes) {
|
||||
for (i = 0, l = this.scopes.length; i < l; i++) {
|
||||
this.scopes[i].stop(true)
|
||||
}
|
||||
this.scopes.length = 0
|
||||
}
|
||||
|
||||
// nested scope, dereference from parent to avoid memory leaks
|
||||
if (!this.detached && this.parent && !fromParent) {
|
||||
// optimized O(1) removal
|
||||
|
@ -141,7 +148,6 @@ export class EffectScope {
|
|||
}
|
||||
}
|
||||
this.parent = undefined
|
||||
this._active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -213,7 +213,7 @@ export function watch(
|
|||
const scope = getCurrentScope()
|
||||
const watchHandle: WatchHandle = () => {
|
||||
effect.stop()
|
||||
if (scope) {
|
||||
if (scope && scope.active) {
|
||||
remove(scope.effects, effect)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import {
|
|||
} from '@vue/runtime-test'
|
||||
import {
|
||||
type DebuggerEvent,
|
||||
EffectFlags,
|
||||
ITERATE_KEY,
|
||||
type Ref,
|
||||
type ShallowRef,
|
||||
|
@ -1341,7 +1340,7 @@ describe('api: watch', () => {
|
|||
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 ', () => {
|
||||
|
|
|
@ -87,6 +87,49 @@ describe('renderer: teleport', () => {
|
|||
`</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) {
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
h,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onServerPrefetch,
|
||||
openBlock,
|
||||
reactive,
|
||||
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 () => {
|
||||
const Comp1 = {
|
||||
template: `
|
||||
|
@ -1284,6 +1324,84 @@ describe('SSR hydration', () => {
|
|||
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 () => {
|
||||
let resolve: any
|
||||
const AsyncComp = defineAsyncComponent(
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
serializeInner as inner,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onUnmounted,
|
||||
openBlock,
|
||||
|
@ -1199,7 +1200,7 @@ describe('renderer: optimized mode', () => {
|
|||
createBlock('div', null, [
|
||||
createVNode('div', null, [
|
||||
cache[0] ||
|
||||
(setBlockTracking(-1),
|
||||
(setBlockTracking(-1, true),
|
||||
((cache[0] = createVNode('div', null, [
|
||||
createVNode(Child),
|
||||
])).cacheIndex = 0),
|
||||
|
@ -1233,4 +1234,64 @@ describe('renderer: optimized mode', () => {
|
|||
expect(inner(root)).toBe('<!--v-if-->')
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import {
|
||||
KeepAlive,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
h,
|
||||
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>',
|
||||
)
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -629,7 +629,7 @@ describe('vnode', () => {
|
|||
const vnode =
|
||||
(openBlock(),
|
||||
createBlock('div', null, [
|
||||
setBlockTracking(-1),
|
||||
setBlockTracking(-1, true),
|
||||
(vnode1 = (openBlock(), createBlock('div'))),
|
||||
setBlockTracking(1),
|
||||
vnode1,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/runtime-core",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "@vue/runtime-core",
|
||||
"main": "index.js",
|
||||
"module": "dist/runtime-core.esm-bundler.js",
|
||||
|
|
|
@ -149,9 +149,7 @@ export function defineEmits() {
|
|||
return null as any
|
||||
}
|
||||
|
||||
export type ComponentTypeEmits =
|
||||
| ((...args: any[]) => any)
|
||||
| Record<string, any[]>
|
||||
export type ComponentTypeEmits = ((...args: any[]) => any) | Record<string, any>
|
||||
|
||||
type RecordToUnion<T extends Record<string, any>> = T[keyof T]
|
||||
|
||||
|
|
|
@ -198,8 +198,7 @@ const BaseTransitionImpl: ComponentOptions = {
|
|||
setTransitionHooks(innerChild, enterHooks)
|
||||
}
|
||||
|
||||
const oldChild = instance.subTree
|
||||
const oldInnerChild = oldChild && getInnerChild(oldChild)
|
||||
let oldInnerChild = instance.subTree && getInnerChild(instance.subTree)
|
||||
|
||||
// handle mode
|
||||
if (
|
||||
|
@ -208,7 +207,7 @@ const BaseTransitionImpl: ComponentOptions = {
|
|||
!isSameVNodeType(innerChild, oldInnerChild) &&
|
||||
recursiveGetSubtree(instance).type !== Comment
|
||||
) {
|
||||
const leavingHooks = resolveTransitionHooks(
|
||||
let leavingHooks = resolveTransitionHooks(
|
||||
oldInnerChild,
|
||||
rawProps,
|
||||
state,
|
||||
|
@ -228,6 +227,7 @@ const BaseTransitionImpl: ComponentOptions = {
|
|||
instance.update()
|
||||
}
|
||||
delete leavingHooks.afterLeave
|
||||
oldInnerChild = undefined
|
||||
}
|
||||
return emptyPlaceholder(child)
|
||||
} else if (mode === 'in-out' && innerChild.type !== Comment) {
|
||||
|
@ -238,18 +238,27 @@ const BaseTransitionImpl: ComponentOptions = {
|
|||
) => {
|
||||
const leavingVNodesCache = getLeavingNodesForType(
|
||||
state,
|
||||
oldInnerChild,
|
||||
oldInnerChild!,
|
||||
)
|
||||
leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild
|
||||
leavingVNodesCache[String(oldInnerChild!.key)] = oldInnerChild!
|
||||
// early removal callback
|
||||
el[leaveCbKey] = () => {
|
||||
earlyRemove()
|
||||
el[leaveCbKey] = undefined
|
||||
delete enterHooks.delayedLeave
|
||||
oldInnerChild = undefined
|
||||
}
|
||||
enterHooks.delayedLeave = () => {
|
||||
delayedLeave()
|
||||
delete enterHooks.delayedLeave
|
||||
oldInnerChild = undefined
|
||||
}
|
||||
enterHooks.delayedLeave = delayedLeave
|
||||
}
|
||||
} else {
|
||||
oldInnerChild = undefined
|
||||
}
|
||||
} else if (oldInnerChild) {
|
||||
oldInnerChild = undefined
|
||||
}
|
||||
|
||||
return child
|
||||
|
|
|
@ -164,11 +164,32 @@ export const TeleportImpl = {
|
|||
}
|
||||
|
||||
if (isTeleportDeferred(n2.props)) {
|
||||
queuePostRenderEffect(mountToTarget, parentSuspense)
|
||||
queuePostRenderEffect(() => {
|
||||
mountToTarget()
|
||||
n2.el!.__isMounted = true
|
||||
}, parentSuspense)
|
||||
} else {
|
||||
mountToTarget()
|
||||
}
|
||||
} 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
|
||||
n2.el = n1.el
|
||||
n2.targetStart = n1.targetStart
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
normalizeVNode,
|
||||
} from './vnode'
|
||||
import { flushPostFlushCbs } from './scheduler'
|
||||
import type { ComponentInternalInstance } from './component'
|
||||
import type { ComponentInternalInstance, ComponentOptions } from './component'
|
||||
import { invokeDirectiveHook } from './directives'
|
||||
import { warn } from './warning'
|
||||
import {
|
||||
|
@ -41,6 +41,7 @@ import {
|
|||
import type { TeleportImpl, TeleportVNode } from './components/Teleport'
|
||||
import { isAsyncWrapper } from './apiAsyncComponent'
|
||||
import { isReactive } from '@vue/reactivity'
|
||||
import { updateHOCHostEl } from './componentRenderUtils'
|
||||
|
||||
export type RootHydrateFunction = (
|
||||
vnode: VNode<Node, Element>,
|
||||
|
@ -307,7 +308,10 @@ export function createHydrationFunctions(
|
|||
// if component is async, it may get moved / unmounted before its
|
||||
// inner component is loaded, so we need to give it a placeholder
|
||||
// vnode that matches its adopted DOM.
|
||||
if (isAsyncWrapper(vnode)) {
|
||||
if (
|
||||
isAsyncWrapper(vnode) &&
|
||||
!(vnode.type as ComponentOptions).__asyncResolved
|
||||
) {
|
||||
let subTree
|
||||
if (isFragmentStart) {
|
||||
subTree = createVNode(Fragment)
|
||||
|
@ -716,6 +720,11 @@ export function createHydrationFunctions(
|
|||
getContainerType(container),
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { isRef, toRaw } from '@vue/reactivity'
|
|||
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
|
||||
import type { SchedulerJob } from './scheduler'
|
||||
import { queuePostRenderEffect } from './renderer'
|
||||
import { getComponentPublicInstance } from './component'
|
||||
import { type ComponentOptions, getComponentPublicInstance } from './component'
|
||||
import { knownTemplateRefs } from './helpers/useTemplateRef'
|
||||
|
||||
/**
|
||||
|
@ -42,8 +42,18 @@ export function setRef(
|
|||
}
|
||||
|
||||
if (isAsyncWrapper(vnode) && !isUnmount) {
|
||||
// when mounting async components, nothing needs to be done,
|
||||
// because the template ref is forwarded to inner component
|
||||
// #4999 if an async component already resolved and cached by KeepAlive,
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
|
@ -301,7 +301,7 @@ export let isBlockTreeEnabled = 1
|
|||
*
|
||||
* ``` js
|
||||
* _cache[1] || (
|
||||
* setBlockTracking(-1),
|
||||
* setBlockTracking(-1, true),
|
||||
* _cache[1] = createVNode(...),
|
||||
* setBlockTracking(1),
|
||||
* _cache[1]
|
||||
|
@ -310,11 +310,11 @@ export let isBlockTreeEnabled = 1
|
|||
*
|
||||
* @private
|
||||
*/
|
||||
export function setBlockTracking(value: number): void {
|
||||
export function setBlockTracking(value: number, inVOnce = false): void {
|
||||
isBlockTreeEnabled += value
|
||||
if (value < 0 && currentBlock) {
|
||||
if (value < 0 && currentBlock && inVOnce) {
|
||||
// mark current block so it doesn't take fast path and skip possible
|
||||
// nested components duriung unmount
|
||||
// nested components during unmount
|
||||
currentBlock.hasOnce = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -396,6 +396,38 @@ describe('defineCustomElement', () => {
|
|||
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', () => {
|
||||
const E = defineCustomElement(
|
||||
props => {
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
defineCustomElement,
|
||||
h,
|
||||
nextTick,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
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
|
||||
test('with custom element', async () => {
|
||||
const state = reactive({ color: 'red' })
|
||||
|
@ -405,4 +444,25 @@ describe('useCssVars', () => {
|
|||
`<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`)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/runtime-dom",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "@vue/runtime-dom",
|
||||
"main": "index.js",
|
||||
"module": "dist/runtime-dom.esm-bundler.js",
|
||||
|
|
|
@ -505,6 +505,8 @@ export class VueElement
|
|||
}
|
||||
// reflect
|
||||
if (shouldReflect) {
|
||||
const ob = this._ob
|
||||
ob && ob.disconnect()
|
||||
if (val === true) {
|
||||
this.setAttribute(hyphenate(key), '')
|
||||
} else if (typeof val === 'string' || typeof val === 'number') {
|
||||
|
@ -512,6 +514,7 @@ export class VueElement
|
|||
} else if (!val) {
|
||||
this.removeAttribute(hyphenate(key))
|
||||
}
|
||||
ob && ob.observe(this, { attributes: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -181,7 +181,13 @@ export function resolveTransitionProps(
|
|||
onAppearCancelled = onEnterCancelled,
|
||||
} = 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 ? appearActiveClass : enterActiveClass)
|
||||
done && done()
|
||||
|
@ -240,7 +246,10 @@ export function resolveTransitionProps(
|
|||
},
|
||||
onEnter: makeEnterHook(false),
|
||||
onAppear: makeEnterHook(true),
|
||||
onLeave(el: Element & { _isLeaving?: boolean }, done) {
|
||||
onLeave(
|
||||
el: Element & { _isLeaving?: boolean; _enterCancelled?: boolean },
|
||||
done,
|
||||
) {
|
||||
el._isLeaving = true
|
||||
const resolve = () => finishLeave(el, done)
|
||||
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
|
||||
// the css will not get the final state (#10677)
|
||||
addTransitionClass(el, leaveActiveClass)
|
||||
// force reflow so *-leave-from classes immediately take effect (#2593)
|
||||
forceReflow()
|
||||
if (!el._enterCancelled) {
|
||||
// force reflow so *-leave-from classes immediately take effect (#2593)
|
||||
forceReflow()
|
||||
addTransitionClass(el, leaveActiveClass)
|
||||
} else {
|
||||
addTransitionClass(el, leaveActiveClass)
|
||||
forceReflow()
|
||||
}
|
||||
nextFrame(() => {
|
||||
if (!el._isLeaving) {
|
||||
// cancelled
|
||||
|
@ -269,11 +283,11 @@ export function resolveTransitionProps(
|
|||
callHook(onLeave, [el, resolve])
|
||||
},
|
||||
onEnterCancelled(el) {
|
||||
finishEnter(el, false)
|
||||
finishEnter(el, false, undefined, true)
|
||||
callHook(onEnterCancelled, [el])
|
||||
},
|
||||
onAppearCancelled(el) {
|
||||
finishEnter(el, true)
|
||||
finishEnter(el, true, undefined, true)
|
||||
callHook(onAppearCancelled, [el])
|
||||
},
|
||||
onLeaveCancelled(el) {
|
||||
|
|
|
@ -3,13 +3,14 @@ import {
|
|||
Static,
|
||||
type VNode,
|
||||
getCurrentInstance,
|
||||
onBeforeMount,
|
||||
onBeforeUpdate,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
queuePostFlushCb,
|
||||
warn,
|
||||
watchPostEffect,
|
||||
watch,
|
||||
} 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' : '')
|
||||
/**
|
||||
|
@ -48,11 +49,15 @@ export function useCssVars(getter: (ctx: any) => Record<string, string>): void {
|
|||
updateTeleports(vars)
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
watchPostEffect(setVars)
|
||||
// handle cases where child component root is affected
|
||||
// and triggers reflow in onMounted
|
||||
onBeforeUpdate(() => {
|
||||
queuePostFlushCb(setVars)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// run setVars synchronously here, but run as post-effect on changes
|
||||
watch(setVars, NOOP, { flush: 'post' })
|
||||
const ob = new MutationObserver(setVars)
|
||||
ob.observe(instance.subTree.el!.parentNode, { childList: true })
|
||||
onUnmounted(() => ob.disconnect())
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
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', () => {
|
||||
let stringBuffer = createBuffer()
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { bench, describe } from 'vitest'
|
||||
|
||||
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 {
|
||||
const buffer = createBuffer()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/server-renderer",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "@vue/server-renderer",
|
||||
"main": "index.js",
|
||||
"module": "dist/server-renderer.esm-bundler.js",
|
||||
|
|
|
@ -153,10 +153,10 @@ describe('normalizeStyle', () => {
|
|||
})
|
||||
|
||||
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('')).toBe('')
|
||||
expect(stringifyStyle('color: blue;')).toBe('')
|
||||
expect(stringifyStyle('color: blue;')).toBe('color: blue;')
|
||||
})
|
||||
|
||||
test('should return valid CSS string for normalized style object', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/shared",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "internal utils shared across @vue packages",
|
||||
"main": "index.js",
|
||||
"module": "dist/shared.esm-bundler.js",
|
||||
|
|
|
@ -45,10 +45,10 @@ export function parseStringStyle(cssText: string): NormalizedStyle {
|
|||
export function stringifyStyle(
|
||||
styles: NormalizedStyle | string | undefined,
|
||||
): string {
|
||||
if (!styles) return ''
|
||||
if (isString(styles)) return styles
|
||||
|
||||
let ret = ''
|
||||
if (!styles || isString(styles)) {
|
||||
return ret
|
||||
}
|
||||
for (const key in styles) {
|
||||
const value = styles[key]
|
||||
if (isString(value) || typeof value === 'number') {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vue/compat",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "Vue 3 compatibility build for Vue 2",
|
||||
"main": "index.js",
|
||||
"module": "dist/vue.runtime.esm-bundler.js",
|
||||
|
|
|
@ -3,7 +3,7 @@ import path from 'node:path'
|
|||
import { Transition, createApp, h, nextTick, ref } from 'vue'
|
||||
|
||||
describe('e2e: Transition', () => {
|
||||
const { page, html, classList, isVisible, timeout, nextFrame, click } =
|
||||
const { page, html, classList, style, isVisible, timeout, nextFrame, click } =
|
||||
setupPuppeteer()
|
||||
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 () => {
|
||||
createApp({
|
||||
render() {
|
||||
|
@ -3121,4 +3170,124 @@ describe('e2e: Transition', () => {
|
|||
},
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -39,6 +39,7 @@ interface PuppeteerUtils {
|
|||
value(selector: string): Promise<string>
|
||||
html(selector: string): Promise<string>
|
||||
classList(selector: string): Promise<string[]>
|
||||
style(selector: string, property: keyof CSSStyleDeclaration): Promise<any>
|
||||
children(selector: string): Promise<any[]>
|
||||
isVisible(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])
|
||||
}
|
||||
|
||||
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> {
|
||||
const display = await page.$eval(selector, node => {
|
||||
return window.getComputedStyle(node).display
|
||||
|
@ -195,6 +209,7 @@ export function setupPuppeteer(args?: string[]): PuppeteerUtils {
|
|||
value,
|
||||
html,
|
||||
classList,
|
||||
style,
|
||||
children,
|
||||
isVisible,
|
||||
isChecked,
|
||||
|
|
|
@ -16,11 +16,21 @@
|
|||
.test-appear,
|
||||
.test-enter,
|
||||
.test-leave-active,
|
||||
.test-reflow-enter,
|
||||
.test-reflow-leave-to,
|
||||
.hello,
|
||||
.bye.active,
|
||||
.changed-enter {
|
||||
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 {
|
||||
animation: test-enter 50ms;
|
||||
-webkit-animation: test-enter 50ms;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "vue",
|
||||
"version": "3.5.12",
|
||||
"version": "3.5.13",
|
||||
"description": "The progressive JavaScript framework for building modern web UI.",
|
||||
"main": "index.js",
|
||||
"module": "dist/vue.runtime.esm-bundler.js",
|
||||
|
|
|
@ -358,8 +358,8 @@ importers:
|
|||
specifier: ^6.0.0
|
||||
version: 6.0.0(postcss@8.4.48)
|
||||
postcss-selector-parser:
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
pug:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
|
@ -3027,6 +3027,10 @@ packages:
|
|||
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
|
@ -6214,6 +6218,11 @@ snapshots:
|
|||
cssesc: 3.0.0
|
||||
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@8.4.48:
|
||||
|
|
|
@ -46,6 +46,12 @@ const pkg = require(resolve(`package.json`))
|
|||
const packageOptions = pkg.buildOptions || {}
|
||||
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()
|
||||
|
||||
/** @type {Record<PackageFormat, OutputOptions>} */
|
||||
|
@ -138,11 +144,7 @@ function createConfig(format, output, plugins = []) {
|
|||
(isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) &&
|
||||
!packageOptions.enableNonBrowserBranches
|
||||
|
||||
output.banner = `/**
|
||||
* ${pkg.name} v${masterVersion}
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/`
|
||||
output.banner = banner
|
||||
|
||||
output.exports = isCompatPackage ? 'auto' : 'named'
|
||||
if (isCJSBuild) {
|
||||
|
@ -375,24 +377,21 @@ function createMinifiedConfig(/** @type {PackageFormat} */ format) {
|
|||
{
|
||||
name: 'swc-minify',
|
||||
|
||||
async renderChunk(
|
||||
contents,
|
||||
_,
|
||||
{ format, sourcemap, sourcemapExcludeSources },
|
||||
) {
|
||||
const { code, map } = await minifySwc(contents, {
|
||||
async renderChunk(contents, _, { format }) {
|
||||
const { code } = await minifySwc(contents, {
|
||||
module: format === 'es',
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
compress: {
|
||||
ecma: 2016,
|
||||
pure_getters: true,
|
||||
},
|
||||
safari10: true,
|
||||
mangle: true,
|
||||
sourceMap: !!sourcemap,
|
||||
inlineSourcesContent: !sourcemapExcludeSources,
|
||||
})
|
||||
|
||||
return { code, map: map || null }
|
||||
return { code: banner + code, map: null }
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue