Merge remote-tracking branch 'upstream/minor'

This commit is contained in:
三咲智子 Kevin Deng 2024-05-11 22:11:30 +08:00
commit 7cd70505d5
No known key found for this signature in database
45 changed files with 1133 additions and 370 deletions

View File

@ -1,3 +1,33 @@
# [3.5.0-alpha.2](https://github.com/vuejs/core/compare/v3.4.26...v3.5.0-alpha.2) (2024-05-04)
### Bug Fixes
* **types:** fix app.component() typing with inline defineComponent ([908f70a](https://github.com/vuejs/core/commit/908f70adc06038d1ea253d96f4024367f4a7545d)), closes [#10843](https://github.com/vuejs/core/issues/10843)
* **types:** fix compat with generated types that rely on CreateComponentPublicInstance ([c146186](https://github.com/vuejs/core/commit/c146186396d0c1a65423b8c9a21251c5a6467336)), closes [#10842](https://github.com/vuejs/core/issues/10842)
* **types:** props in defineOptions type should be optional ([124c4ca](https://github.com/vuejs/core/commit/124c4cac833a28ae9bc8edc576c1d0c7c41f5985)), closes [#10841](https://github.com/vuejs/core/issues/10841)
### Features
* **runtime-core:** add app.onUnmount() for registering cleanup functions ([#4619](https://github.com/vuejs/core/issues/4619)) ([582a3a3](https://github.com/vuejs/core/commit/582a3a382b1adda565bac576b913a88d9e8d7a9e)), closes [#4516](https://github.com/vuejs/core/issues/4516)
## [3.4.26](https://github.com/vuejs/core/compare/v3.4.25...v3.4.26) (2024-04-29)
### Bug Fixes
* **compiler-core:** fix bail constant for globals ([fefce06](https://github.com/vuejs/core/commit/fefce06b41e3b75de3d748dc6399628ec5056e78))
* **compiler-core:** remove unnecessary constant bail check ([09b4df8](https://github.com/vuejs/core/commit/09b4df809e59ef5f4bc91acfc56dc8f82a8e243a)), closes [#10807](https://github.com/vuejs/core/issues/10807)
* **runtime-core:** attrs should be readonly in functional components ([#10767](https://github.com/vuejs/core/issues/10767)) ([e8fd644](https://github.com/vuejs/core/commit/e8fd6446d14a6899e5e8ab1ee394d90088e01844))
* **runtime-core:** ensure slot compiler marker writable ([#10825](https://github.com/vuejs/core/issues/10825)) ([9c2de62](https://github.com/vuejs/core/commit/9c2de6244cd44bc5fbfd82b5850c710ce725044f)), closes [#10818](https://github.com/vuejs/core/issues/10818)
* **runtime-core:** properly handle inherit transition during clone VNode ([#10809](https://github.com/vuejs/core/issues/10809)) ([638a79f](https://github.com/vuejs/core/commit/638a79f64a7e184f2a2c65e21d764703f4bda561)), closes [#3716](https://github.com/vuejs/core/issues/3716) [#10497](https://github.com/vuejs/core/issues/10497) [#4091](https://github.com/vuejs/core/issues/4091)
* **Transition:** re-fix [#10620](https://github.com/vuejs/core/issues/10620) ([#10832](https://github.com/vuejs/core/issues/10832)) ([accf839](https://github.com/vuejs/core/commit/accf8396ae1c9dd49759ba0546483f1d2c70c9bc)), closes [#10632](https://github.com/vuejs/core/issues/10632) [#10827](https://github.com/vuejs/core/issues/10827)
# [3.5.0-alpha.1](https://github.com/vuejs/core/compare/v3.4.25...v3.5.0-alpha.1) (2024-04-29) # [3.5.0-alpha.1](https://github.com/vuejs/core/compare/v3.4.25...v3.5.0-alpha.1) (2024-04-29)

View File

@ -1,7 +1,7 @@
{ {
"private": true, "private": true,
"version": "3.0.0-vapor", "version": "3.0.0-vapor",
"packageManager": "pnpm@9.0.5", "packageManager": "pnpm@9.0.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/dev.js vue vue-vapor", "dev": "node scripts/dev.js vue vue-vapor",
@ -72,16 +72,16 @@
"@types/minimist": "^1.2.5", "@types/minimist": "^1.2.5",
"@types/node": "^20.12.7", "@types/node": "^20.12.7",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@vitest/coverage-istanbul": "^1.5.0", "@vitest/coverage-istanbul": "^1.5.2",
"@vitest/ui": "^1.5.0", "@vitest/ui": "^1.5.2",
"@vue/consolidate": "1.0.0", "@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^4.1.0", "conventional-changelog-cli": "^4.1.0",
"enquirer": "^2.4.1", "enquirer": "^2.4.1",
"esbuild": "^0.20.2", "esbuild": "^0.20.2",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.0.0", "eslint": "^9.1.1",
"eslint-plugin-import-x": "^0.5.0", "eslint-plugin-import-x": "^0.5.0",
"eslint-plugin-vitest": "^0.5.3", "eslint-plugin-vitest": "^0.5.4",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"execa": "^8.0.1", "execa": "^8.0.1",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
@ -96,23 +96,23 @@
"prettier": "^3.2.5", "prettier": "^3.2.5",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pug": "^3.0.2", "pug": "^3.0.2",
"puppeteer": "~22.6.5", "puppeteer": "~22.7.1",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"rollup": "^4.16.1", "rollup": "^4.17.1",
"rollup-plugin-dts": "^6.1.0", "rollup-plugin-dts": "^6.1.0",
"rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0", "rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.0", "semver": "^7.6.0",
"serve": "^14.2.1", "serve": "^14.2.3",
"simple-git-hooks": "^2.11.1", "simple-git-hooks": "^2.11.1",
"terser": "^5.30.3", "terser": "^5.30.4",
"todomvc-app-css": "^2.4.3", "todomvc-app-css": "^2.4.3",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"tsx": "^4.7.2", "tsx": "^4.7.3",
"typescript": "~5.4.5", "typescript": "~5.4.5",
"typescript-eslint": "^7.6.0", "typescript-eslint": "^7.7.1",
"vite": "^5.2.10", "vite": "^5.2.10",
"vitest": "^1.5.0" "vitest": "^1.5.2"
}, },
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {

View File

@ -421,6 +421,31 @@ describe('compiler: expression transform', () => {
}) })
}) })
// #10807
test('should not bail constant on strings w/ ()', () => {
const node = parseWithExpressionTransform(
`{{ { foo: 'ok()' } }}`,
) as InterpolationNode
expect(node.content).toMatchObject({
constType: ConstantTypes.CAN_STRINGIFY,
})
})
test('should bail constant for global identifiers w/ new or call expressions', () => {
const node = parseWithExpressionTransform(
`{{ new Date().getFullYear() }}`,
) as InterpolationNode
expect(node.content).toMatchObject({
children: [
'new ',
{ constType: ConstantTypes.NOT_CONSTANT },
'().',
{ constType: ConstantTypes.NOT_CONSTANT },
'()',
],
})
})
describe('ES Proposals support', () => { describe('ES Proposals support', () => {
test('bigInt', () => { test('bigInt', () => {
const node = parseWithExpressionTransform( const node = parseWithExpressionTransform(

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-core", "name": "@vue/compiler-core",
"version": "3.5.0-alpha.1", "version": "3.5.0-alpha.2",
"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

@ -10,6 +10,9 @@ import type {
} from '@babel/types' } from '@babel/types'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
/**
* Return value indicates whether the AST walked can be a constant
*/
export function walkIdentifiers( export function walkIdentifiers(
root: Node, root: Node,
onIdentifier: ( onIdentifier: (

View File

@ -179,7 +179,7 @@ const tokenizer = new Tokenizer(stack, {
const name = currentOpenTag!.tag const name = currentOpenTag!.tag
currentOpenTag!.isSelfClosing = true currentOpenTag!.isSelfClosing = true
endOpenTag(end) endOpenTag(end)
if (stack[0]?.tag === name) { if (stack[0] && stack[0].tag === name) {
onCloseTag(stack.shift()!, end) onCloseTag(stack.shift()!, end)
} }
}, },
@ -587,14 +587,14 @@ function endOpenTag(end: number) {
function onText(content: string, start: number, end: number) { function onText(content: string, start: number, end: number) {
if (__BROWSER__) { if (__BROWSER__) {
const tag = stack[0]?.tag const tag = stack[0] && stack[0].tag
if (tag !== 'script' && tag !== 'style' && content.includes('&')) { if (tag !== 'script' && tag !== 'style' && content.includes('&')) {
content = currentOptions.decodeEntities!(content, false) content = currentOptions.decodeEntities!(content, false)
} }
} }
const parent = stack[0] || currentRoot const parent = stack[0] || currentRoot
const lastNode = parent.children[parent.children.length - 1] const lastNode = parent.children[parent.children.length - 1]
if (lastNode?.type === NodeTypes.TEXT) { if (lastNode && lastNode.type === NodeTypes.TEXT) {
// merge // merge
lastNode.content += content lastNode.content += content
setLocEnd(lastNode.loc, end) setLocEnd(lastNode.loc, end)
@ -771,7 +771,8 @@ function isComponent({ tag, props }: ElementNode): boolean {
tag === 'component' || tag === 'component' ||
isUpperCase(tag.charCodeAt(0)) || isUpperCase(tag.charCodeAt(0)) ||
isCoreComponent(tag) || isCoreComponent(tag) ||
currentOptions.isBuiltInComponent?.(tag) || (currentOptions.isBuiltInComponent &&
currentOptions.isBuiltInComponent(tag)) ||
(currentOptions.isNativeTag && !currentOptions.isNativeTag(tag)) (currentOptions.isNativeTag && !currentOptions.isNativeTag(tag))
) { ) {
return true return true
@ -828,8 +829,8 @@ function condenseWhitespace(
if (node.type === NodeTypes.TEXT) { if (node.type === NodeTypes.TEXT) {
if (!inPre) { if (!inPre) {
if (isAllWhitespace(node.content)) { if (isAllWhitespace(node.content)) {
const prev = nodes[i - 1]?.type const prev = nodes[i - 1] && nodes[i - 1].type
const next = nodes[i + 1]?.type const next = nodes[i + 1] && nodes[i + 1].type
// Remove if: // Remove if:
// - the whitespace is the first or last node, or: // - the whitespace is the first or last node, or:
// - (condense mode) the whitespace is between two comments, or: // - (condense mode) the whitespace is between two comments, or:
@ -1063,7 +1064,7 @@ export function baseParse(input: string, options?: ParserOptions): RootNode {
currentOptions.ns === Namespaces.SVG || currentOptions.ns === Namespaces.SVG ||
currentOptions.ns === Namespaces.MATH_ML currentOptions.ns === Namespaces.MATH_ML
const delimiters = options?.delimiters const delimiters = options && options.delimiters
if (delimiters) { if (delimiters) {
tokenizer.delimiterOpen = toCharCodes(delimiters[0]) tokenizer.delimiterOpen = toCharCodes(delimiters[0])
tokenizer.delimiterClose = toCharCodes(delimiters[1]) tokenizer.delimiterClose = toCharCodes(delimiters[1])

View File

@ -48,10 +48,6 @@ export const isLiteralWhitelisted = /*#__PURE__*/ makeMap(
'true,false,null,this', 'true,false,null,this',
) )
// a heuristic safeguard to bail constant expressions on presence of
// likely function invocation and member access
const constantBailRE = /\w\s*\(|\.[^\d]/
export const transformExpression: NodeTransform = (node, context) => { export const transformExpression: NodeTransform = (node, context) => {
if (node.type === NodeTypes.INTERPOLATION) { if (node.type === NodeTypes.INTERPOLATION) {
node.content = processExpression( node.content = processExpression(
@ -228,8 +224,6 @@ export function processExpression(
// fast path if expression is a simple identifier. // fast path if expression is a simple identifier.
const rawExp = node.content const rawExp = node.content
// bail constant on parens (function invocation) and dot (member access)
const bailConstant = constantBailRE.test(rawExp)
let ast = node.ast let ast = node.ast
@ -319,7 +313,12 @@ export function processExpression(
} else { } else {
// The identifier is considered constant unless it's pointing to a // The identifier is considered constant unless it's pointing to a
// local scope variable (a v-for alias, or a v-slot prop) // local scope variable (a v-for alias, or a v-slot prop)
if (!(needPrefix && isLocal) && !bailConstant) { if (
!(needPrefix && isLocal) &&
parent.type !== 'CallExpression' &&
parent.type !== 'NewExpression' &&
parent.type !== 'MemberExpression'
) {
;(node as QualifiedId).isConstant = true ;(node as QualifiedId).isConstant = true
} }
// also generate sub-expressions for other identifiers for better // also generate sub-expressions for other identifiers for better
@ -373,9 +372,7 @@ export function processExpression(
ret.ast = ast ret.ast = ast
} else { } else {
ret = node ret = node
ret.constType = bailConstant ret.constType = ConstantTypes.CAN_STRINGIFY
? ConstantTypes.NOT_CONSTANT
: ConstantTypes.CAN_STRINGIFY
} }
ret.identifiers = Object.keys(knownIds) ret.identifiers = Object.keys(knownIds)
return ret return ret

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-dom", "name": "@vue/compiler-dom",
"version": "3.5.0-alpha.1", "version": "3.5.0-alpha.2",
"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.0-alpha.1", "version": "3.5.0-alpha.2",
"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",

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-ssr", "name": "@vue/compiler-ssr",
"version": "3.5.0-alpha.1", "version": "3.5.0-alpha.2",
"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

@ -4,4 +4,4 @@ Tests TypeScript types to ensure the types remain as expected.
- This directory is included in the root `tsconfig.json`, where package imports are aliased to `src` directories, so in IDEs and the `pnpm check` script the types are validated against source code. - This directory is included in the root `tsconfig.json`, where package imports are aliased to `src` directories, so in IDEs and the `pnpm check` script the types are validated against source code.
- When running `tsc` with `packages/dts-test/tsconfig.test.json`, packages are resolved using normal `node` resolution, so the types are validated against actual **built** types. This requires the types to be built first via `pnpm build-types`. - When running `tsc` with `packages/dts-test/tsconfig.test.json`, packages are resolved using normal `node` resolution, so the types are validated against actual **built** types. This requires the types to be built first via `pnpm build-dts`.

View File

@ -1766,3 +1766,210 @@ defineComponent({
expectType<string | null | undefined>(props.foo) expectType<string | null | undefined>(props.foo)
}, },
}) })
import type * as vue from 'vue'
interface ErrorMessageSlotProps {
message: string | undefined
}
/**
* #10842
* component types generated by vue-tsc
* relying on legacy CreateComponentPublicInstance signature
*/
declare const ErrorMessage: {
new (...args: any[]): vue.CreateComponentPublicInstance<
Readonly<
vue.ExtractPropTypes<{
as: {
type: StringConstructor
default: any
}
name: {
type: StringConstructor
required: true
}
}>
>,
() =>
| VNode<
vue.RendererNode,
vue.RendererElement,
{
[key: string]: any
}
>
| vue.Slot<any>
| VNode<
vue.RendererNode,
vue.RendererElement,
{
[key: string]: any
}
>[]
| {
default: () => VNode<
vue.RendererNode,
vue.RendererElement,
{
[key: string]: any
}
>[]
},
unknown,
{},
{},
vue.ComponentOptionsMixin,
vue.ComponentOptionsMixin,
{},
vue.VNodeProps &
vue.AllowedComponentProps &
vue.ComponentCustomProps &
Readonly<
vue.ExtractPropTypes<{
as: {
type: StringConstructor
default: any
}
name: {
type: StringConstructor
required: true
}
}>
>,
{
as: string
},
true,
{},
{},
{
P: {}
B: {}
D: {}
C: {}
M: {}
Defaults: {}
},
Readonly<
vue.ExtractPropTypes<{
as: {
type: StringConstructor
default: any
}
name: {
type: StringConstructor
required: true
}
}>
>,
() =>
| VNode<
vue.RendererNode,
vue.RendererElement,
{
[key: string]: any
}
>
| vue.Slot<any>
| VNode<
vue.RendererNode,
vue.RendererElement,
{
[key: string]: any
}
>[]
| {
default: () => VNode<
vue.RendererNode,
vue.RendererElement,
{
[key: string]: any
}
>[]
},
{},
{},
{},
{
as: string
}
>
__isFragment?: never
__isTeleport?: never
__isSuspense?: never
} & vue.ComponentOptionsBase<
Readonly<
vue.ExtractPropTypes<{
as: {
type: StringConstructor
default: any
}
name: {
type: StringConstructor
required: true
}
}>
>,
() =>
| VNode<
vue.RendererNode,
vue.RendererElement,
{
[key: string]: any
}
>
| vue.Slot<any>
| VNode<
vue.RendererNode,
vue.RendererElement,
{
[key: string]: any
}
>[]
| {
default: () => VNode<
vue.RendererNode,
vue.RendererElement,
{
[key: string]: any
}
>[]
},
unknown,
{},
{},
vue.ComponentOptionsMixin,
vue.ComponentOptionsMixin,
{},
string,
{
as: string
},
{},
string,
{}
> &
vue.VNodeProps &
vue.AllowedComponentProps &
vue.ComponentCustomProps &
(new () => {
$slots: {
default: (arg: ErrorMessageSlotProps) => VNode[]
}
})
;<ErrorMessage name="password" class="error" />
// #10843
createApp({}).component(
'SomeComponent',
defineComponent({
props: {
title: String,
},
setup(props) {
expectType<string | undefined>(props.title)
return {}
},
}),
)

View File

@ -5,6 +5,7 @@ import {
defineComponent, defineComponent,
defineEmits, defineEmits,
defineModel, defineModel,
defineOptions,
defineProps, defineProps,
defineSlots, defineSlots,
toRefs, toRefs,
@ -501,3 +502,21 @@ describe('toRefs w/ type declaration', () => {
}>() }>()
expectType<Ref<File | File[] | undefined>>(toRefs(props).file) expectType<Ref<File | File[] | undefined>>(toRefs(props).file)
}) })
describe('defineOptions', () => {
defineOptions({
name: 'MyComponent',
inheritAttrs: true,
})
defineOptions({
// @ts-expect-error props should be defined via defineProps()
props: ['props'],
// @ts-expect-error emits should be defined via defineEmits()
emits: ['emits'],
// @ts-expect-error slots should be defined via defineSlots()
slots: { default: 'default' },
// @ts-expect-error expose should be defined via defineExpose()
expose: ['expose'],
})
})

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/reactivity", "name": "@vue/reactivity",
"version": "3.5.0-alpha.1", "version": "3.5.0-alpha.2",
"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

@ -322,39 +322,32 @@ export function baseWatch(
export function traverse( export function traverse(
value: unknown, value: unknown,
depth?: number, depth = Infinity,
currentDepth = 0,
seen?: Set<unknown>, seen?: Set<unknown>,
) { ) {
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) { if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value return value
} }
if (depth && depth > 0) {
if (currentDepth >= depth) {
return value
}
currentDepth++
}
seen = seen || new Set() seen = seen || new Set()
if (seen.has(value)) { if (seen.has(value)) {
return value return value
} }
seen.add(value) seen.add(value)
depth--
if (isRef(value)) { if (isRef(value)) {
traverse(value.value, depth, currentDepth, seen) traverse(value.value, depth, seen)
} else if (isArray(value)) { } else if (isArray(value)) {
for (let i = 0; i < value.length; i++) { for (let i = 0; i < value.length; i++) {
traverse(value[i], depth, currentDepth, seen) traverse(value[i], depth, seen)
} }
} else if (isSet(value) || isMap(value)) { } else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => { value.forEach((v: any) => {
traverse(v, depth, currentDepth, seen) traverse(v, depth, seen)
}) })
} else if (isPlainObject(value)) { } else if (isPlainObject(value)) {
for (const key in value) { for (const key in value) {
traverse(value[key], depth, currentDepth, seen) traverse(value[key], depth, seen)
} }
} }
return value return value

View File

@ -344,6 +344,36 @@ describe('api: createApp', () => {
).toHaveBeenWarnedTimes(1) ).toHaveBeenWarnedTimes(1)
}) })
test('onUnmount', () => {
const cleanup = vi.fn().mockName('plugin cleanup')
const PluginA: Plugin = app => {
app.provide('foo', 1)
app.onUnmount(cleanup)
}
const PluginB: Plugin = {
install: (app, arg1, arg2) => {
app.provide('bar', arg1 + arg2)
app.onUnmount(cleanup)
},
}
const app = createApp({
render: () => `Test`,
})
app.use(PluginA)
app.use(PluginB)
const root = nodeOps.createElement('div')
app.mount(root)
//also can be added after mount
app.onUnmount(cleanup)
app.unmount()
expect(cleanup).toHaveBeenCalledTimes(3)
})
test('config.errorHandler', () => { test('config.errorHandler', () => {
const error = new Error() const error = new Error()
const count = ref(0) const count = ref(0)

View File

@ -17,7 +17,6 @@ import {
ref, ref,
render, render,
serializeInner, serializeInner,
toRaw,
toRefs, toRefs,
watch, watch,
} from '@vue/runtime-test' } from '@vue/runtime-test'
@ -129,12 +128,12 @@ describe('component props', () => {
render(h(Comp, { foo: 1 }), root) render(h(Comp, { foo: 1 }), root)
expect(props).toEqual({ foo: 1 }) expect(props).toEqual({ foo: 1 })
expect(attrs).toEqual({ foo: 1 }) expect(attrs).toEqual({ foo: 1 })
expect(toRaw(props)).toBe(attrs) expect(props).toBe(attrs)
render(h(Comp, { bar: 2 }), root) render(h(Comp, { bar: 2 }), root)
expect(props).toEqual({ bar: 2 }) expect(props).toEqual({ bar: 2 })
expect(attrs).toEqual({ bar: 2 }) expect(attrs).toEqual({ bar: 2 })
expect(toRaw(props)).toBe(attrs) expect(props).toBe(attrs)
}) })
test('boolean casting', () => { test('boolean casting', () => {

View File

@ -7,7 +7,6 @@ import {
h, h,
nextTick, nextTick,
nodeOps, nodeOps,
onUnmounted,
ref, ref,
render, render,
serialize, serialize,
@ -769,42 +768,6 @@ describe('BaseTransition', () => {
test('w/ KeepAlive', async () => { test('w/ KeepAlive', async () => {
await runTestWithKeepAlive(testOutIn) await runTestWithKeepAlive(testOutIn)
}) })
test('w/ KeepAlive + unmount innerChild', async () => {
const unmountSpy = vi.fn()
const includeRef = ref(['TrueBranch'])
const trueComp = {
name: 'TrueBranch',
setup() {
onUnmounted(unmountSpy)
const count = ref(0)
return () => h('div', count.value)
},
}
const toggle = ref(true)
const { props } = mockProps({ mode: 'out-in' }, true /*withKeepAlive*/)
const root = nodeOps.createElement('div')
const App = {
render() {
return h(BaseTransition, props, () => {
return h(
KeepAlive,
{ include: includeRef.value },
toggle.value ? h(trueComp) : h('div'),
)
})
},
}
render(h(App), root)
// trigger toggle
toggle.value = false
includeRef.value = []
await nextTick()
expect(unmountSpy).toHaveBeenCalledTimes(1)
})
}) })
// #6835 // #6835

View File

@ -356,7 +356,7 @@ describe('hot module replacement', () => {
triggerEvent(root.children[1] as TestElement, 'click') triggerEvent(root.children[1] as TestElement, 'click')
await nextTick() await nextTick()
await new Promise(r => setTimeout(r, 0)) await new Promise(r => setTimeout(r, 0))
expect(serializeInner(root)).toBe(`<button></button><!---->`) expect(serializeInner(root)).toBe(`<button></button><!--v-if-->`)
expect(unmountSpy).toHaveBeenCalledTimes(1) expect(unmountSpy).toHaveBeenCalledTimes(1)
expect(mountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1)
expect(activeSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1)

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/runtime-core", "name": "@vue/runtime-core",
"version": "3.5.0-alpha.1", "version": "3.5.0-alpha.2",
"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

@ -26,6 +26,7 @@ import { version } from '.'
import { installAppCompatProperties } from './compat/global' import { installAppCompatProperties } from './compat/global'
import type { NormalizedPropsOptions } from './componentProps' import type { NormalizedPropsOptions } from './componentProps'
import type { ObjectEmitsOptions } from './componentEmits' import type { ObjectEmitsOptions } from './componentEmits'
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import type { DefineComponent } from './apiDefineComponent' import type { DefineComponent } from './apiDefineComponent'
export interface App<HostElement = any> { export interface App<HostElement = any> {
@ -40,7 +41,10 @@ export interface App<HostElement = any> {
mixin(mixin: ComponentOptions): this mixin(mixin: ComponentOptions): this
component(name: string): Component | undefined component(name: string): Component | undefined
component(name: string, component: Component | DefineComponent): this component<T extends Component | DefineComponent>(
name: string,
component: T,
): this
directive<T = any, V = any>(name: string): Directive<T, V> | undefined directive<T = any, V = any>(name: string): Directive<T, V> | undefined
directive<T = any, V = any>(name: string, directive: Directive<T, V>): this directive<T = any, V = any>(name: string, directive: Directive<T, V>): this
mount( mount(
@ -49,6 +53,7 @@ export interface App<HostElement = any> {
namespace?: boolean | ElementNamespace, namespace?: boolean | ElementNamespace,
): ComponentPublicInstance ): ComponentPublicInstance
unmount(): void unmount(): void
onUnmount(cb: () => void): void
provide<T>(key: InjectionKey<T> | string, value: T): this provide<T>(key: InjectionKey<T> | string, value: T): this
/** /**
@ -213,6 +218,7 @@ export function createAppAPI<HostElement>(
const context = createAppContext() const context = createAppContext()
const installedPlugins = new WeakSet() const installedPlugins = new WeakSet()
const pluginCleanupFns: Array<() => any> = []
let isMounted = false let isMounted = false
@ -365,8 +371,23 @@ export function createAppAPI<HostElement>(
} }
}, },
onUnmount(cleanupFn: () => void) {
if (__DEV__ && typeof cleanupFn !== 'function') {
warn(
`Expected function as first argument to app.onUnmount(), ` +
`but got ${typeof cleanupFn}`,
)
}
pluginCleanupFns.push(cleanupFn)
},
unmount() { unmount() {
if (isMounted) { if (isMounted) {
callWithAsyncErrorHandling(
pluginCleanupFns,
app._instance,
ErrorCodes.APP_UNMOUNT_CLEANUP,
)
render(null, app._container) render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = null app._instance = null

View File

@ -31,7 +31,7 @@ import { extend, isFunction } from '@vue/shared'
import type { VNodeProps } from './vnode' import type { VNodeProps } from './vnode'
import type { import type {
ComponentPublicInstanceConstructor, ComponentPublicInstanceConstructor,
CreateComponentPublicInstance, CreateComponentPublicInstanceWithMixins,
} from './componentPublicInstance' } from './componentPublicInstance'
import type { SlotsType } from './componentSlots' import type { SlotsType } from './componentSlots'
import type { Directive } from './directives' import type { Directive } from './directives'
@ -68,7 +68,7 @@ export type DefineComponent<
Provide extends ComponentProvideOptions = ComponentProvideOptions, Provide extends ComponentProvideOptions = ComponentProvideOptions,
MakeDefaultsOptional extends boolean = true, MakeDefaultsOptional extends boolean = true,
> = ComponentPublicInstanceConstructor< > = ComponentPublicInstanceConstructor<
CreateComponentPublicInstance< CreateComponentPublicInstanceWithMixins<
Props, Props,
RawBindings, RawBindings,
D, D,
@ -116,7 +116,7 @@ export type DefineSetupFnComponent<
PP = PublicProps, PP = PublicProps,
> = new ( > = new (
props: Props & PP, props: Props & PP,
) => CreateComponentPublicInstance< ) => CreateComponentPublicInstanceWithMixins<
Props, Props,
{}, {},
{}, {},
@ -240,7 +240,7 @@ export function defineComponent<
Provide Provide
> & > &
ThisType< ThisType<
CreateComponentPublicInstance< CreateComponentPublicInstanceWithMixins<
ResolvedProps, ResolvedProps,
SetupBindings, SetupBindings,
Data, Data,

View File

@ -210,7 +210,7 @@ export function defineOptions<
/** /**
* props should be defined via defineProps(). * props should be defined via defineProps().
*/ */
props: never props?: never
/** /**
* emits should be defined via defineEmits(). * emits should be defined via defineEmits().
*/ */

View File

@ -77,7 +77,12 @@ export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
nextTick: typeof nextTick nextTick: typeof nextTick
use(plugin: Plugin, ...options: any[]): CompatVue use<Options extends unknown[]>(
plugin: Plugin<Options>,
...options: Options
): CompatVue
use<Options>(plugin: Plugin<Options>, options: Options): CompatVue
mixin(mixin: ComponentOptions): CompatVue mixin(mixin: ComponentOptions): CompatVue
component(name: string): Component | undefined component(name: string): Component | undefined
@ -176,11 +181,11 @@ export function createCompatVue(
Vue.version = `2.6.14-compat:${__VERSION__}` Vue.version = `2.6.14-compat:${__VERSION__}`
Vue.config = singletonApp.config Vue.config = singletonApp.config
Vue.use = (p, ...options) => { Vue.use = (plugin: Plugin, ...options: any[]) => {
if (p && isFunction(p.install)) { if (plugin && isFunction(plugin.install)) {
p.install(Vue as any, ...options) plugin.install(Vue as any, ...options)
} else if (isFunction(p)) { } else if (isFunction(plugin)) {
p(Vue as any, ...options) plugin(Vue as any, ...options)
} }
return Vue return Vue
} }

View File

@ -63,7 +63,7 @@ import type {
import type { Directive } from './directives' import type { Directive } from './directives'
import { import {
type ComponentPublicInstance, type ComponentPublicInstance,
type CreateComponentPublicInstance, type CreateComponentPublicInstanceWithMixins,
type IntersectionMixin, type IntersectionMixin,
type UnwrapMixinsType, type UnwrapMixinsType,
isReservedPrefix, isReservedPrefix,
@ -264,7 +264,7 @@ export type ComponentOptions<
Provide Provide
> & > &
ThisType< ThisType<
CreateComponentPublicInstance< CreateComponentPublicInstanceWithMixins<
{}, {},
RawBindings, RawBindings,
D, D,
@ -373,7 +373,7 @@ interface LegacyOptions<
// since that leads to some sort of circular inference and breaks ThisType // since that leads to some sort of circular inference and breaks ThisType
// for the entire component. // for the entire component.
data?: ( data?: (
this: CreateComponentPublicInstance< this: CreateComponentPublicInstanceWithMixins<
Props, Props,
{}, {},
{}, {},
@ -382,7 +382,7 @@ interface LegacyOptions<
Mixin, Mixin,
Extends Extends
>, >,
vm: CreateComponentPublicInstance< vm: CreateComponentPublicInstanceWithMixins<
Props, Props,
{}, {},
{}, {},
@ -1153,7 +1153,7 @@ export type ComponentOptionsWithoutProps<
*/ */
__typeEmits?: TE __typeEmits?: TE
} & ThisType< } & ThisType<
CreateComponentPublicInstance< CreateComponentPublicInstanceWithMixins<
PE, PE,
RawBindings, RawBindings,
D, D,
@ -1215,7 +1215,7 @@ export type ComponentOptionsWithArrayProps<
> & { > & {
props: PropNames[] props: PropNames[]
} & ThisType< } & ThisType<
CreateComponentPublicInstance< CreateComponentPublicInstanceWithMixins<
Props, Props,
RawBindings, RawBindings,
D, D,
@ -1278,7 +1278,7 @@ export type ComponentOptionsWithObjectProps<
> & { > & {
props: PropsOptions & ThisType<void> props: PropsOptions & ThisType<void>
} & ThisType< } & ThisType<
CreateComponentPublicInstance< CreateComponentPublicInstanceWithMixins<
Props, Props,
RawBindings, RawBindings,
D, D,

View File

@ -150,7 +150,71 @@ export type ComponentPublicInstanceConstructor<
new (...args: any[]): T new (...args: any[]): T
} }
/**
* @deprecated This is no longer used internally, but exported and relied on by
* existing library types generated by vue-tsc.
*/
export type CreateComponentPublicInstance< export type CreateComponentPublicInstance<
P = {},
B = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
PublicProps = P,
Defaults = {},
MakeDefaultsOptional extends boolean = false,
I extends ComponentInjectOptions = {},
S extends SlotsType = {},
PublicMixin = IntersectionMixin<Mixin> & IntersectionMixin<Extends>,
PublicP = UnwrapMixinsType<PublicMixin, 'P'> & EnsureNonVoid<P>,
PublicB = UnwrapMixinsType<PublicMixin, 'B'> & EnsureNonVoid<B>,
PublicD = UnwrapMixinsType<PublicMixin, 'D'> & EnsureNonVoid<D>,
PublicC extends ComputedOptions = UnwrapMixinsType<PublicMixin, 'C'> &
EnsureNonVoid<C>,
PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> &
EnsureNonVoid<M>,
PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> &
EnsureNonVoid<Defaults>,
> = ComponentPublicInstance<
PublicP,
PublicB,
PublicD,
PublicC,
PublicM,
E,
PublicProps,
PublicDefaults,
MakeDefaultsOptional,
ComponentOptionsBase<
P,
B,
D,
C,
M,
Mixin,
Extends,
E,
string,
Defaults,
{},
string,
S
>,
I,
S
>
/**
* This is the same as `CreateComponentPublicInstance` but adds local components,
* global directives, exposed, and provide inference.
* It changes the arguments order so that we don't need to repeat mixin
* inference everywhere internally, but it has to be a new type to avoid
* breaking types that relies on previous arguments order (#10842)
*/
export type CreateComponentPublicInstanceWithMixins<
P = {}, P = {},
B = {}, B = {},
D = {}, D = {},
@ -167,6 +231,8 @@ export type CreateComponentPublicInstance<
LC extends Record<string, Component> = {}, LC extends Record<string, Component> = {},
Directives extends Record<string, Directive> = {}, Directives extends Record<string, Directive> = {},
Exposed extends string = string, Exposed extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
// mixin inference
PublicMixin = IntersectionMixin<Mixin> & IntersectionMixin<Extends>, PublicMixin = IntersectionMixin<Mixin> & IntersectionMixin<Extends>,
PublicP = UnwrapMixinsType<PublicMixin, 'P'> & EnsureNonVoid<P>, PublicP = UnwrapMixinsType<PublicMixin, 'P'> & EnsureNonVoid<P>,
PublicB = UnwrapMixinsType<PublicMixin, 'B'> & EnsureNonVoid<B>, PublicB = UnwrapMixinsType<PublicMixin, 'B'> & EnsureNonVoid<B>,
@ -177,7 +243,6 @@ export type CreateComponentPublicInstance<
EnsureNonVoid<M>, EnsureNonVoid<M>,
PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> & PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> &
EnsureNonVoid<Defaults>, EnsureNonVoid<Defaults>,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
> = ComponentPublicInstance< > = ComponentPublicInstance<
PublicP, PublicP,
PublicB, PublicB,

View File

@ -121,7 +121,7 @@ export function renderComponentRoot(
? { ? {
get attrs() { get attrs() {
markAttrsAccessed() markAttrsAccessed()
return attrs return shallowReadonly(attrs)
}, },
slots, slots,
emit, emit,
@ -171,7 +171,7 @@ export function renderComponentRoot(
propsOptions, propsOptions,
) )
} }
root = cloneVNode(root, fallthroughAttrs) root = cloneVNode(root, fallthroughAttrs, false, true)
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) { } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
const allAttrs = Object.keys(attrs) const allAttrs = Object.keys(attrs)
const eventAttrs: string[] = [] const eventAttrs: string[] = []
@ -226,10 +226,15 @@ export function renderComponentRoot(
getComponentName(instance.type), getComponentName(instance.type),
) )
} }
root = cloneVNode(root, { root = cloneVNode(
root,
{
class: cls, class: cls,
style: style, style: style,
}) },
false,
true,
)
} }
} }
@ -242,7 +247,7 @@ export function renderComponentRoot(
) )
} }
// clone before mutating since the root may be a hoisted vnode // clone before mutating since the root may be a hoisted vnode
root = cloneVNode(root) root = cloneVNode(root, null, false, true)
root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
} }
// inherit transition data // inherit transition data

View File

@ -171,7 +171,7 @@ export const initSlots = (
if (type) { if (type) {
extend(slots, children as InternalSlots) extend(slots, children as InternalSlots)
// make compiler marker non-enumerable // make compiler marker non-enumerable
def(slots, '_', type) def(slots, '_', type, true)
} else { } else {
normalizeObjectSlots(children as RawSlots, slots, instance) normalizeObjectSlots(children as RawSlots, slots, instance)
} }

View File

@ -205,7 +205,7 @@ const BaseTransitionImpl: ComponentOptions = {
// update old tree's hooks in case of dynamic transition // update old tree's hooks in case of dynamic transition
setTransitionHooks(oldInnerChild, leavingHooks) setTransitionHooks(oldInnerChild, leavingHooks)
// switching between different views // switching between different views
if (mode === 'out-in') { if (mode === 'out-in' && innerChild.type !== Comment) {
state.isLeaving = true state.isLeaving = true
// return placeholder node and queue update when leave finishes // return placeholder node and queue update when leave finishes
leavingHooks.afterLeave = () => { leavingHooks.afterLeave = () => {

View File

@ -254,7 +254,7 @@ const KeepAliveImpl: ComponentOptions = {
pendingCacheKey = null pendingCacheKey = null
if (!slots.default) { if (!slots.default) {
return (current = null) return null
} }
const children = slots.default() const children = slots.default()

View File

@ -479,7 +479,7 @@ function createSuspenseBoundary(
let parentSuspenseId: number | undefined let parentSuspenseId: number | undefined
const isSuspensible = isVNodeSuspensible(vnode) const isSuspensible = isVNodeSuspensible(vnode)
if (isSuspensible) { if (isSuspensible) {
if (parentSuspense?.pendingBranch) { if (parentSuspense && parentSuspense.pendingBranch) {
parentSuspenseId = parentSuspense.pendingId parentSuspenseId = parentSuspense.pendingId
parentSuspense.deps++ parentSuspense.deps++
} }
@ -898,5 +898,6 @@ function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
} }
function isVNodeSuspensible(vnode: VNode) { function isVNodeSuspensible(vnode: VNode) {
return vnode.props?.suspensible != null && vnode.props.suspensible !== false const suspensible = vnode.props && vnode.props.suspensible
return suspensible != null && suspensible !== false
} }

View File

@ -52,8 +52,12 @@ export type DirectiveHook<
prevVNode: Prev, prevVNode: Prev,
) => void ) => void
export type SSRDirectiveHook = ( export type SSRDirectiveHook<
binding: DirectiveBinding, Value = any,
Modifiers extends string = string,
Arg extends string = string,
> = (
binding: DirectiveBinding<Value, Modifiers, Arg>,
vnode: VNode, vnode: VNode,
) => Data | undefined ) => Data | undefined
@ -63,6 +67,12 @@ export interface ObjectDirective<
Modifiers extends string = string, Modifiers extends string = string,
Arg extends string = string, Arg extends string = string,
> { > {
/**
* @internal without this, ts-expect-error in directives.test-d.ts somehow
* fails when running tsc, but passes in IDE and when testing against built
* dts. Could be a TS bug.
*/
__mod?: Modifiers
created?: DirectiveHook<HostElement, null, Value, Modifiers, Arg> created?: DirectiveHook<HostElement, null, Value, Modifiers, Arg>
beforeMount?: DirectiveHook<HostElement, null, Value, Modifiers, Arg> beforeMount?: DirectiveHook<HostElement, null, Value, Modifiers, Arg>
mounted?: DirectiveHook<HostElement, null, Value, Modifiers, Arg> mounted?: DirectiveHook<HostElement, null, Value, Modifiers, Arg>
@ -82,7 +92,7 @@ export interface ObjectDirective<
> >
beforeUnmount?: DirectiveHook<HostElement, null, Value, Modifiers, Arg> beforeUnmount?: DirectiveHook<HostElement, null, Value, Modifiers, Arg>
unmounted?: DirectiveHook<HostElement, null, Value, Modifiers, Arg> unmounted?: DirectiveHook<HostElement, null, Value, Modifiers, Arg>
getSSRProps?: SSRDirectiveHook getSSRProps?: SSRDirectiveHook<Value, Modifiers, Arg>
deep?: boolean deep?: boolean
} }

View File

@ -27,6 +27,7 @@ export enum ErrorCodes {
FUNCTION_REF, FUNCTION_REF,
ASYNC_COMPONENT_LOADER, ASYNC_COMPONENT_LOADER,
SCHEDULER, SCHEDULER,
APP_UNMOUNT_CLEANUP,
} }
export type ErrorTypes = LifecycleHooks | ErrorCodes | BaseWatchErrorCodes export type ErrorTypes = LifecycleHooks | ErrorCodes | BaseWatchErrorCodes
@ -63,6 +64,7 @@ export const ErrorTypeStrings: Record<ErrorTypes, string> = {
[ErrorCodes.SCHEDULER]: [ErrorCodes.SCHEDULER]:
'scheduler flush. This is likely a Vue internals bug. ' + 'scheduler flush. This is likely a Vue internals bug. ' +
'Please open an issue at https://github.com/vuejs/core .', 'Please open an issue at https://github.com/vuejs/core .',
[ErrorCodes.APP_UNMOUNT_CLEANUP]: 'app unmount cleanup function',
} }
export function callWithErrorHandling( export function callWithErrorHandling(

View File

@ -270,6 +270,10 @@ export type {
ComputedOptions, ComputedOptions,
RuntimeCompilerOptions, RuntimeCompilerOptions,
ComponentInjectOptions, ComponentInjectOptions,
// deprecated
ComponentOptionsWithoutProps,
ComponentOptionsWithArrayProps,
ComponentOptionsWithObjectProps,
} from './componentOptions' } from './componentOptions'
export type { export type {
EmitsOptions, EmitsOptions,
@ -280,6 +284,7 @@ export type {
ComponentPublicInstance, ComponentPublicInstance,
ComponentCustomProperties, ComponentCustomProperties,
CreateComponentPublicInstance, CreateComponentPublicInstance,
CreateComponentPublicInstanceWithMixins,
} from './componentPublicInstance' } from './componentPublicInstance'
export type { export type {
Renderer, Renderer,

View File

@ -624,10 +624,11 @@ export function cloneVNode<T, U>(
vnode: VNode<T, U>, vnode: VNode<T, U>,
extraProps?: (Data & VNodeProps) | null, extraProps?: (Data & VNodeProps) | null,
mergeRef = false, mergeRef = false,
cloneTransition = false,
): VNode<T, U> { ): VNode<T, U> {
// This is intentionally NOT using spread or extend to avoid the runtime // This is intentionally NOT using spread or extend to avoid the runtime
// key enumeration cost. // key enumeration cost.
const { props, ref, patchFlag, children } = vnode const { props, ref, patchFlag, children, transition } = vnode
const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
const cloned: VNode<T, U> = { const cloned: VNode<T, U> = {
__v_isVNode: true, __v_isVNode: true,
@ -670,7 +671,7 @@ export function cloneVNode<T, U>(
dynamicChildren: vnode.dynamicChildren, dynamicChildren: vnode.dynamicChildren,
appContext: vnode.appContext, appContext: vnode.appContext,
dirs: vnode.dirs, dirs: vnode.dirs,
transition: vnode.transition, transition,
// These should technically only be non-null on mounted VNodes. However, // These should technically only be non-null on mounted VNodes. However,
// they *should* be copied for kept-alive vnodes. So we just always copy // they *should* be copied for kept-alive vnodes. So we just always copy
@ -685,9 +686,18 @@ export function cloneVNode<T, U>(
ctx: vnode.ctx, ctx: vnode.ctx,
ce: vnode.ce, ce: vnode.ce,
} }
// if the vnode will be replaced by the cloned one, it is necessary
// to clone the transition to ensure that the vnode referenced within
// the transition hooks is fresh.
if (transition && cloneTransition) {
cloned.transition = transition.clone(cloned as VNode)
}
if (__COMPAT__) { if (__COMPAT__) {
defineLegacyVNodeProperties(cloned as VNode) defineLegacyVNodeProperties(cloned as VNode)
} }
return cloned return cloned
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/runtime-dom", "name": "@vue/runtime-dom",
"version": "3.5.0-alpha.1", "version": "3.5.0-alpha.2",
"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

@ -9,7 +9,7 @@ import {
type ComponentProvideOptions, type ComponentProvideOptions,
type ComputedOptions, type ComputedOptions,
type ConcreteComponent, type ConcreteComponent,
type CreateComponentPublicInstance, type CreateComponentPublicInstanceWithMixins,
type DefineComponent, type DefineComponent,
type Directive, type Directive,
type EmitsOptions, type EmitsOptions,
@ -97,7 +97,7 @@ export function defineCustomElement<
Provide Provide
> & > &
ThisType< ThisType<
CreateComponentPublicInstance< CreateComponentPublicInstanceWithMixins<
Readonly<ResolvedProps>, Readonly<ResolvedProps>,
SetupBindings, SetupBindings,
Data, Data,

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/server-renderer", "name": "@vue/server-renderer",
"version": "3.5.0-alpha.1", "version": "3.5.0-alpha.2",
"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

@ -13,7 +13,7 @@
"vite": "^5.2.10" "vite": "^5.2.10"
}, },
"dependencies": { "dependencies": {
"@vue/repl": "^4.1.1", "@vue/repl": "^4.1.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"vue": "workspace:*" "vue": "workspace:*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/shared", "name": "@vue/shared",
"version": "3.5.0-alpha.1", "version": "3.5.0-alpha.2",
"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

@ -146,10 +146,16 @@ export const invokeArrayFns = (fns: Function[], arg?: any) => {
} }
} }
export const def = (obj: object, key: string | symbol, value: any) => { export const def = (
obj: object,
key: string | symbol,
value: any,
writable = false,
) => {
Object.defineProperty(obj, key, { Object.defineProperty(obj, key, {
configurable: true, configurable: true,
enumerable: false, enumerable: false,
writable,
value, value,
}) })
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compat", "name": "@vue/compat",
"version": "3.5.0-alpha.1", "version": "3.5.0-alpha.2",
"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

@ -1214,6 +1214,111 @@ describe('e2e: Transition', () => {
}, },
E2E_TIMEOUT, E2E_TIMEOUT,
) )
// #3716
test(
'wrapping transition + fallthrough attrs',
async () => {
await page().goto(baseUrl)
await page().waitForSelector('#app')
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
components: {
'my-transition': {
template: `
<transition foo="1" name="test">
<slot></slot>
</transition>
`,
},
},
template: `
<div id="container">
<my-transition>
<div v-if="toggle">content</div>
</my-transition>
</div>
<button id="toggleBtn" @click="click">button</button>
`,
setup: () => {
const toggle = ref(true)
const click = () => (toggle.value = !toggle.value)
return { toggle, click }
},
}).mount('#app')
})
expect(await html('#container')).toBe('<div foo="1">content</div>')
await click('#toggleBtn')
// toggle again before leave finishes
await nextTick()
await click('#toggleBtn')
await transitionFinish()
expect(await html('#container')).toBe(
'<div foo="1" class="">content</div>',
)
},
E2E_TIMEOUT,
)
test(
'w/ KeepAlive + unmount innerChild',
async () => {
const unmountSpy = vi.fn()
await page().exposeFunction('unmountSpy', unmountSpy)
await page().evaluate(() => {
const { unmountSpy } = window as any
const { createApp, ref, h, onUnmounted } = (window as any).Vue
createApp({
template: `
<div id="container">
<transition mode="out-in">
<KeepAlive :include="includeRef">
<TrueBranch v-if="toggle"></TrueBranch>
</KeepAlive>
</transition>
</div>
<button id="toggleBtn" @click="click">button</button>
`,
components: {
TrueBranch: {
name: 'TrueBranch',
setup() {
onUnmounted(unmountSpy)
const count = ref(0)
return () => h('div', count.value)
},
},
},
setup: () => {
const includeRef = ref(['TrueBranch'])
const toggle = ref(true)
const click = () => {
toggle.value = !toggle.value
if (toggle.value) {
includeRef.value = ['TrueBranch']
} else {
includeRef.value = []
}
}
return { toggle, click, unmountSpy, includeRef }
},
}).mount('#app')
})
await transitionFinish()
expect(await html('#container')).toBe('<div>0</div>')
await click('#toggleBtn')
await transitionFinish()
expect(await html('#container')).toBe('<!--v-if-->')
expect(unmountSpy).toBeCalledTimes(1)
},
E2E_TIMEOUT,
)
}) })
describe('transition with Suspense', () => { describe('transition with Suspense', () => {

View File

@ -1,6 +1,6 @@
{ {
"name": "vue", "name": "vue",
"version": "3.5.0-alpha.1", "version": "3.5.0-alpha.2",
"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",

File diff suppressed because it is too large Load Diff