// implementation, close to no-op
export function defineComponent(options: unknown) {
diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts
index 155c8cd19..a8b7fcdef 100644
--- a/packages/runtime-core/src/apiSetupHelpers.ts
+++ b/packages/runtime-core/src/apiSetupHelpers.ts
@@ -139,7 +139,7 @@ type InferDefault = T extends
| boolean
| symbol
| Function
- ? T
+ ? T | ((props: P) => T)
: (props: P) => T
type PropsWithDefaults = Base & {
diff --git a/packages/runtime-core/src/compat/global.ts b/packages/runtime-core/src/compat/global.ts
index e0fc699fa..823181d23 100644
--- a/packages/runtime-core/src/compat/global.ts
+++ b/packages/runtime-core/src/compat/global.ts
@@ -381,9 +381,10 @@ function installLegacyAPIs(app: App) {
function applySingletonAppMutations(app: App) {
// copy over asset registries and deopt flag
- ;['mixins', 'components', 'directives', 'filters', 'deopt'].forEach(key => {
+ app._context.mixins = [...singletonApp._context.mixins]
+ ;['components', 'directives', 'filters'].forEach(key => {
// @ts-ignore
- app._context[key] = singletonApp._context[key]
+ app._context[key] = Object.create(singletonApp._context[key])
})
// copy over global config mutations
@@ -398,7 +399,7 @@ function applySingletonAppMutations(app: App) {
}
const val = singletonApp.config[key as keyof AppConfig]
// @ts-ignore
- app.config[key] = val
+ app.config[key] = isObject(val) ? Object.create(val) : val
// compat for runtime ignoredElements -> isCustomElement
if (
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 11a6cafbc..eededdf34 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -106,6 +106,10 @@ export interface ComponentInternalOptions {
* This one should be exposed so that devtools can make use of it
*/
__file?: string
+ /**
+ * name inferred from filename
+ */
+ __name?: string
}
export interface FunctionalComponent
@@ -949,11 +953,12 @@ const classify = (str: string): string =>
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
export function getComponentName(
- Component: ConcreteComponent
-): string | undefined {
+ Component: ConcreteComponent,
+ includeInferred = true
+): string | false | undefined {
return isFunction(Component)
? Component.displayName || Component.name
- : Component.name
+ : Component.name || (includeInferred && Component.__name)
}
/* istanbul ignore next */
diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts
index ed95174b1..0d47e18c4 100644
--- a/packages/runtime-core/src/componentOptions.ts
+++ b/packages/runtime-core/src/componentOptions.ts
@@ -58,7 +58,8 @@ import { EmitsOptions, EmitsToProps } from './componentEmits'
import { Directive } from './directives'
import {
CreateComponentPublicInstance,
- ComponentPublicInstance
+ ComponentPublicInstance,
+ isReservedPrefix
} from './componentPublicInstance'
import { warn } from './warning'
import { VNodeChild } from './vnode'
@@ -117,9 +118,8 @@ export interface ComponentOptionsBase<
Extends extends ComponentOptionsMixin,
E extends EmitsOptions,
EE extends string = string,
- Defaults = {},
- Provide extends ComponentProvideOptions = ComponentProvideOptions
-> extends LegacyOptions,
+ Defaults = {}
+> extends LegacyOptions,
ComponentInternalOptions,
ComponentCustomOptions {
setup?: (
@@ -225,7 +225,6 @@ export type ComponentOptionsWithoutProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
- Provide extends ComponentProvideOptions = ComponentProvideOptions,
PE = Props & EmitsToProps
> = ComponentOptionsBase<
PE,
@@ -237,8 +236,7 @@ export type ComponentOptionsWithoutProps<
Extends,
E,
EE,
- {},
- Provide
+ {}
> & {
props?: undefined
} & ThisType<
@@ -255,7 +253,6 @@ export type ComponentOptionsWithArrayProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
- Provide extends ComponentProvideOptions = ComponentProvideOptions,
Props = Readonly<{ [key in PropNames]?: any }> & EmitsToProps
> = ComponentOptionsBase<
Props,
@@ -267,8 +264,7 @@ export type ComponentOptionsWithArrayProps<
Extends,
E,
EE,
- {},
- Provide
+ {}
> & {
props: PropNames[]
} & ThisType<
@@ -294,7 +290,6 @@ export type ComponentOptionsWithObjectProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
- Provide extends ComponentProvideOptions = ComponentProvideOptions,
Props = Readonly> & EmitsToProps,
Defaults = ExtractDefaultPropTypes
> = ComponentOptionsBase<
@@ -307,8 +302,7 @@ export type ComponentOptionsWithObjectProps<
Extends,
E,
EE,
- Defaults,
- Provide
+ Defaults
> & {
props: PropsOptions & ThisType
} & ThisType<
@@ -408,8 +402,7 @@ interface LegacyOptions<
C extends ComputedOptions,
M extends MethodOptions,
Mixin extends ComponentOptionsMixin,
- Extends extends ComponentOptionsMixin,
- Provide extends ComponentProvideOptions = ComponentProvideOptions
+ Extends extends ComponentOptionsMixin
> {
compatConfig?: CompatConfig
@@ -443,7 +436,7 @@ interface LegacyOptions<
computed?: C
methods?: M
watch?: ComponentWatchOptions
- provide?: Provide
+ provide?: ComponentProvideOptions
inject?: ComponentInjectOptions
// assets
@@ -681,7 +674,7 @@ export function applyOptions(instance: ComponentInternalInstance) {
for (const key in data) {
checkDuplicateProperties!(OptionTypes.DATA, key)
// expose data on ctx during dev
- if (key[0] !== '$' && key[0] !== '_') {
+ if (!isReservedPrefix(key[0])) {
Object.defineProperty(ctx, key, {
configurable: true,
enumerable: true,
diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts
index 33e8ce5fa..1d9136731 100644
--- a/packages/runtime-core/src/componentPublicInstance.ts
+++ b/packages/runtime-core/src/componentPublicInstance.ts
@@ -34,8 +34,7 @@ import {
OptionTypesKeys,
resolveMergedOptions,
shouldCacheAccess,
- MergedComponentOptionsOverride,
- ComponentProvideOptions
+ MergedComponentOptionsOverride
} from './componentOptions'
import { EmitsOptions, EmitFn } from './componentEmits'
import { Slots } from './componentSlots'
@@ -151,8 +150,7 @@ export type CreateComponentPublicInstance<
PublicM extends MethodOptions = UnwrapMixinsType &
EnsureNonVoid,
PublicDefaults = UnwrapMixinsType &
- EnsureNonVoid,
- Provide extends ComponentProvideOptions = ComponentProvideOptions
+ EnsureNonVoid
> = ComponentPublicInstance<
PublicP,
PublicB,
@@ -163,19 +161,7 @@ export type CreateComponentPublicInstance<
PublicProps,
PublicDefaults,
MakeDefaultsOptional,
- ComponentOptionsBase<
- P,
- B,
- D,
- C,
- M,
- Mixin,
- Extends,
- E,
- string,
- Defaults,
- Provide
- >
+ ComponentOptionsBase
>
// public properties exposed on the proxy, which is used as the render context
@@ -274,6 +260,8 @@ export interface ComponentRenderContext {
_: ComponentInternalInstance
}
+export const isReservedPrefix = (key: string) => key === '_' || key === '$'
+
export const PublicInstanceProxyHandlers: ProxyHandler = {
get({ _: instance }: ComponentRenderContext, key: string) {
const { ctx, setupState, data, props, accessCache, type, appContext } =
@@ -385,11 +373,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler = {
// to infinite warning loop
key.indexOf('__v') !== 0)
) {
- if (
- data !== EMPTY_OBJ &&
- (key[0] === '$' || key[0] === '_') &&
- hasOwn(data, key)
- ) {
+ if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) {
warn(
`Property ${JSON.stringify(
key
@@ -571,7 +555,7 @@ export function exposeSetupStateOnRenderContext(
const { ctx, setupState } = instance
Object.keys(toRaw(setupState)).forEach(key => {
if (!setupState.__isScriptSetup) {
- if (key[0] === '$' || key[0] === '_') {
+ if (isReservedPrefix(key[0])) {
warn(
`setup() return property ${JSON.stringify(
key
diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts
index 8e338c31d..d9de968a0 100644
--- a/packages/runtime-core/src/componentRenderUtils.ts
+++ b/packages/runtime-core/src/componentRenderUtils.ts
@@ -38,6 +38,8 @@ export function markAttrsAccessed() {
accessedAttrs = true
}
+type SetRootFn = ((root: VNode) => void) | undefined
+
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
@@ -121,7 +123,7 @@ export function renderComponentRoot(
// in dev mode, comments are preserved, and it's possible for a template
// to have comments along side the root element which makes it a fragment
let root = result
- let setRoot: ((root: VNode) => void) | undefined = undefined
+ let setRoot: SetRootFn = undefined
if (
__DEV__ &&
result.patchFlag > 0 &&
@@ -246,9 +248,7 @@ export function renderComponentRoot(
* template into a fragment root, but we need to locate the single element
* root for attrs and scope id processing.
*/
-const getChildRoot = (
- vnode: VNode
-): [VNode, ((root: VNode) => void) | undefined] => {
+const getChildRoot = (vnode: VNode): [VNode, SetRootFn] => {
const rawChildren = vnode.children as VNodeArrayChildren
const dynamicChildren = vnode.dynamicChildren
const childRoot = filterSingleRoot(rawChildren)
@@ -257,7 +257,7 @@ const getChildRoot = (
}
const index = rawChildren.indexOf(childRoot)
const dynamicIndex = dynamicChildren ? dynamicChildren.indexOf(childRoot) : -1
- const setRoot = (updatedRoot: VNode) => {
+ const setRoot: SetRootFn = (updatedRoot: VNode) => {
rawChildren[index] = updatedRoot
if (dynamicChildren) {
if (dynamicIndex > -1) {
diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts
index 4b2c72885..3fec48140 100644
--- a/packages/runtime-core/src/components/KeepAlive.ts
+++ b/packages/runtime-core/src/components/KeepAlive.ts
@@ -96,8 +96,11 @@ const KeepAliveImpl: ComponentOptions = {
// if the internal renderer is not registered, it indicates that this is server-side rendering,
// for KeepAlive, we just need to render its children
- if (!sharedContext.renderer) {
- return slots.default
+ if (__SSR__ && !sharedContext.renderer) {
+ return () => {
+ const children = slots.default && slots.default()
+ return children && children.length === 1 ? children[0] : children
+ }
}
const cache: Cache = new Map()
diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts
index 68d50a63f..06b69aff4 100644
--- a/packages/runtime-core/src/components/Teleport.ts
+++ b/packages/runtime-core/src/components/Teleport.ts
@@ -353,7 +353,26 @@ function hydrateTeleport(
vnode.targetAnchor = targetNode
} else {
vnode.anchor = nextSibling(node)
- vnode.targetAnchor = hydrateChildren(
+
+ // lookahead until we find the target anchor
+ // we cannot rely on return value of hydrateChildren() because there
+ // could be nested teleports
+ let targetAnchor = targetNode
+ while (targetAnchor) {
+ targetAnchor = nextSibling(targetAnchor)
+ if (
+ targetAnchor &&
+ targetAnchor.nodeType === 8 &&
+ (targetAnchor as Comment).data === 'teleport anchor'
+ ) {
+ vnode.targetAnchor = targetAnchor
+ ;(target as TeleportTargetElement)._lpa =
+ vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
+ break
+ }
+ }
+
+ hydrateChildren(
targetNode,
vnode,
target,
@@ -363,8 +382,6 @@ function hydrateTeleport(
optimized
)
}
- ;(target as TeleportTargetElement)._lpa =
- vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
}
}
return vnode.anchor && nextSibling(vnode.anchor as Node)
diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts
index 878f82415..214c4f446 100644
--- a/packages/runtime-core/src/helpers/resolveAssets.ts
+++ b/packages/runtime-core/src/helpers/resolveAssets.ts
@@ -86,7 +86,10 @@ function resolveAsset(
// explicit self name has highest priority
if (type === COMPONENTS) {
- const selfName = getComponentName(Component)
+ const selfName = getComponentName(
+ Component,
+ false /* do not include inferred name to avoid breaking existing code */
+ )
if (
selfName &&
(selfName === name ||
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index 3d86f0f24..8ada97b16 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -27,7 +27,7 @@ import { isAsyncWrapper } from './apiAsyncComponent'
export type RootHydrateFunction = (
vnode: VNode,
- container: Element | ShadowRoot
+ container: (Element | ShadowRoot) & { _vnode?: VNode }
) => void
const enum DOMNodeTypes {
@@ -55,7 +55,15 @@ export function createHydrationFunctions(
const {
mt: mountComponent,
p: patch,
- o: { patchProp, nextSibling, parentNode, remove, insert, createComment }
+ o: {
+ patchProp,
+ createText,
+ nextSibling,
+ parentNode,
+ remove,
+ insert,
+ createComment
+ }
} = rendererInternals
const hydrate: RootHydrateFunction = (vnode, container) => {
@@ -67,11 +75,13 @@ export function createHydrationFunctions(
)
patch(null, vnode, container)
flushPostFlushCbs()
+ container._vnode = vnode
return
}
hasMismatch = false
hydrateNode(container.firstChild!, vnode, null, null, null)
flushPostFlushCbs()
+ container._vnode = vnode
if (hasMismatch && !__TEST__) {
// this error should show up in production
console.error(`Hydration completed but contains mismatches.`)
@@ -110,7 +120,14 @@ export function createHydrationFunctions(
switch (type) {
case Text:
if (domType !== DOMNodeTypes.TEXT) {
- nextNode = onMismatch()
+ // #5728 empty text node inside a slot can cause hydration failure
+ // because the server rendered HTML won't contain a text node
+ if (vnode.children === '') {
+ insert((vnode.el = createText('')), parentNode(node)!, node)
+ nextNode = node
+ } else {
+ nextNode = onMismatch()
+ }
} else {
if ((node as Text).data !== vnode.children) {
hasMismatch = true
@@ -133,7 +150,7 @@ export function createHydrationFunctions(
}
break
case Static:
- if (domType !== DOMNodeTypes.ELEMENT) {
+ if (domType !== DOMNodeTypes.ELEMENT && domType !== DOMNodeTypes.TEXT) {
nextNode = onMismatch()
} else {
// determine anchor, adopt content
@@ -143,7 +160,10 @@ export function createHydrationFunctions(
const needToAdoptContent = !(vnode.children as string).length
for (let i = 0; i < vnode.staticCount!; i++) {
if (needToAdoptContent)
- vnode.children += (nextNode as Element).outerHTML
+ vnode.children +=
+ nextNode.nodeType === DOMNodeTypes.ELEMENT
+ ? (nextNode as Element).outerHTML
+ : (nextNode as Text).data
if (i === vnode.staticCount! - 1) {
vnode.anchor = nextNode
}
@@ -207,6 +227,15 @@ export function createHydrationFunctions(
? locateClosingAsyncAnchor(node)
: nextSibling(node)
+ // #4293 teleport as component root
+ if (
+ nextNode &&
+ isComment(nextNode) &&
+ nextNode.data === 'teleport end'
+ ) {
+ nextNode = nextSibling(nextNode)
+ }
+
// #3787
// if component is async, it may get moved / unmounted before its
// inner component is loaded, so we need to give it a placeholder
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index ad4817d91..735bea7d1 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -86,7 +86,7 @@ export { h } from './h'
// Advanced render function utilities
export { createVNode, cloneVNode, mergeProps, isVNode } from './vnode'
// VNode types
-export { Fragment, Text, Comment, Static } from './vnode'
+export { Fragment, Text, Comment, Static, VNodeRef } from './vnode'
// Built-in components
export { Teleport, TeleportProps } from './components/Teleport'
export { Suspense, SuspenseProps } from './components/Suspense'
@@ -217,6 +217,7 @@ export {
ComponentOptionsWithArrayProps,
ComponentCustomOptions,
ComponentOptionsBase,
+ ComponentProvideOptions,
RenderFunction,
MethodOptions,
ComputedOptions,
@@ -324,7 +325,7 @@ const _ssrUtils = {
}
/**
- * SSR utils for \@vue/server-renderer. Only exposed in cjs builds.
+ * SSR utils for \@vue/server-renderer. Only exposed in ssr-possible builds.
* @internal
*/
export const ssrUtils = (__SSR__ ? _ssrUtils : null) as typeof _ssrUtils
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 8af783a20..1f3c2a918 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -1064,8 +1064,12 @@ function baseCreateRenderer(
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
- if (__DEV__ && isHmrUpdating) {
- // HMR updated, force full diff
+ if (
+ __DEV__ &&
+ // #5523 dev root fragment may inherit directives
+ (isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT)
+ ) {
+ // HMR updated / Dev root fragment (w/ comments), force full diff
patchFlag = 0
optimized = false
dynamicChildren = null
@@ -1098,8 +1102,6 @@ function baseCreateRenderer(
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
- // #5523 dev root fragment may inherit directives so always force update
- !(__DEV__ && patchFlag & PatchFlags.DEV_ROOT_FRAGMENT) &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts
index 740624c91..1fe432d50 100644
--- a/packages/runtime-core/src/rendererTemplateRef.ts
+++ b/packages/runtime-core/src/rendererTemplateRef.ts
@@ -107,7 +107,7 @@ export function setRef(
if (hasOwn(setupState, ref)) {
setupState[ref] = value
}
- } else if (isRef(ref)) {
+ } else if (_isRef) {
ref.value = value
if (rawRef.k) refs[rawRef.k] = value
} else if (__DEV__) {
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index 0a2543a70..b2c52ac7e 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -43,6 +43,7 @@ import { convertLegacyComponent } from './compat/component'
import { convertLegacyVModelProps } from './compat/componentVModel'
import { defineLegacyVNodeProperties } from './compat/renderFn'
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
+import { ComponentPublicInstance } from './componentPublicInstance'
export const Fragment = Symbol(__DEV__ ? 'Fragment' : undefined) as any as {
__isFragment: true
@@ -68,7 +69,10 @@ export type VNodeTypes =
export type VNodeRef =
| string
| Ref
- | ((ref: object | null, refs: Record) => void)
+ | ((
+ ref: Element | ComponentPublicInstance | null,
+ refs: Record
+ ) => void)
export type VNodeNormalizedRefAtom = {
i: ComponentInternalInstance
diff --git a/packages/runtime-dom/__tests__/directives/vModel.spec.ts b/packages/runtime-dom/__tests__/directives/vModel.spec.ts
index c682fd5b8..f5a0c6fad 100644
--- a/packages/runtime-dom/__tests__/directives/vModel.spec.ts
+++ b/packages/runtime-dom/__tests__/directives/vModel.spec.ts
@@ -291,6 +291,94 @@ describe('vModel', () => {
expect(data.lazy).toEqual('foo')
})
+ it('should work with range', async () => {
+ const component = defineComponent({
+ data() {
+ return { value: 25 }
+ },
+ render() {
+ return [
+ withVModel(
+ h('input', {
+ type: 'range',
+ min: 1,
+ max: 100,
+ class: 'foo',
+ 'onUpdate:modelValue': setValue.bind(this)
+ }),
+ this.value,
+ {
+ number: true
+ }
+ ),
+ withVModel(
+ h('input', {
+ type: 'range',
+ min: 1,
+ max: 100,
+ class: 'bar',
+ 'onUpdate:modelValue': setValue.bind(this)
+ }),
+ this.value,
+ {
+ lazy: true
+ }
+ )
+ ]
+ }
+ })
+ render(h(component), root)
+
+ const foo = root.querySelector('.foo')
+ const bar = root.querySelector('.bar')
+ const data = root._vnode.component.data
+
+ foo.value = 20
+ triggerEvent('input', foo)
+ await nextTick()
+ expect(data.value).toEqual(20)
+
+ foo.value = 200
+ triggerEvent('input', foo)
+ await nextTick()
+ expect(data.value).toEqual(100)
+
+ foo.value = -1
+ triggerEvent('input', foo)
+ await nextTick()
+ expect(data.value).toEqual(1)
+
+ bar.value = 30
+ triggerEvent('change', bar)
+ await nextTick()
+ expect(data.value).toEqual('30')
+
+ bar.value = 200
+ triggerEvent('change', bar)
+ await nextTick()
+ expect(data.value).toEqual('100')
+
+ bar.value = -1
+ triggerEvent('change', bar)
+ await nextTick()
+ expect(data.value).toEqual('1')
+
+ data.value = 60
+ await nextTick()
+ expect(foo.value).toEqual('60')
+ expect(bar.value).toEqual('60')
+
+ data.value = -1
+ await nextTick()
+ expect(foo.value).toEqual('1')
+ expect(bar.value).toEqual('1')
+
+ data.value = 200
+ await nextTick()
+ expect(foo.value).toEqual('100')
+ expect(bar.value).toEqual('100')
+ })
+
it('should work with checkbox', async () => {
const component = defineComponent({
data() {
diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json
index 08ebce778..a8376fa3f 100644
--- a/packages/runtime-dom/package.json
+++ b/packages/runtime-dom/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/runtime-dom",
- "version": "3.2.33",
+ "version": "3.2.37",
"description": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",
@@ -35,8 +35,8 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-dom#readme",
"dependencies": {
- "@vue/shared": "3.2.33",
- "@vue/runtime-core": "3.2.33",
+ "@vue/shared": "3.2.37",
+ "@vue/runtime-core": "3.2.37",
"csstype": "^2.6.8"
}
}
diff --git a/packages/runtime-dom/src/components/Transition.ts b/packages/runtime-dom/src/components/Transition.ts
index f33d41367..0c1a30f6b 100644
--- a/packages/runtime-dom/src/components/Transition.ts
+++ b/packages/runtime-dom/src/components/Transition.ts
@@ -174,9 +174,11 @@ export function resolveTransitionProps(
done && done()
}
- let isLeaving = false
- const finishLeave = (el: Element, done?: () => void) => {
- isLeaving = false
+ const finishLeave = (
+ el: Element & { _isLeaving?: boolean },
+ done?: () => void
+ ) => {
+ el._isLeaving = false
removeTransitionClass(el, leaveFromClass)
removeTransitionClass(el, leaveToClass)
removeTransitionClass(el, leaveActiveClass)
@@ -223,8 +225,8 @@ export function resolveTransitionProps(
},
onEnter: makeEnterHook(false),
onAppear: makeEnterHook(true),
- onLeave(el, done) {
- isLeaving = true
+ onLeave(el: Element & { _isLeaving?: boolean }, done) {
+ el._isLeaving = true
const resolve = () => finishLeave(el, done)
addTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) {
@@ -234,7 +236,7 @@ export function resolveTransitionProps(
forceReflow()
addTransitionClass(el, leaveActiveClass)
nextFrame(() => {
- if (!isLeaving) {
+ if (!el._isLeaving) {
// cancelled
return
}
diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts
index 1a14decd0..722b4d9b4 100644
--- a/packages/runtime-dom/src/directives/vModel.ts
+++ b/packages/runtime-dom/src/directives/vModel.ts
@@ -269,6 +269,24 @@ export const vModelDynamic: ObjectDirective<
}
}
+function resolveDynamicModel(tagName: string, type: string | undefined) {
+ switch (tagName) {
+ case 'SELECT':
+ return vModelSelect
+ case 'TEXTAREA':
+ return vModelText
+ default:
+ switch (type) {
+ case 'checkbox':
+ return vModelCheckbox
+ case 'radio':
+ return vModelRadio
+ default:
+ return vModelText
+ }
+ }
+}
+
function callModelHook(
el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,
binding: DirectiveBinding,
@@ -276,26 +294,10 @@ function callModelHook(
prevVNode: VNode | null,
hook: keyof ObjectDirective
) {
- let modelToUse: ObjectDirective
- switch (el.tagName) {
- case 'SELECT':
- modelToUse = vModelSelect
- break
- case 'TEXTAREA':
- modelToUse = vModelText
- break
- default:
- switch (vnode.props && vnode.props.type) {
- case 'checkbox':
- modelToUse = vModelCheckbox
- break
- case 'radio':
- modelToUse = vModelRadio
- break
- default:
- modelToUse = vModelText
- }
- }
+ const modelToUse = resolveDynamicModel(
+ el.tagName,
+ vnode.props && vnode.props.type
+ )
const fn = modelToUse[hook] as DirectiveHook
fn && fn(el, binding, vnode, prevVNode)
}
@@ -324,4 +326,18 @@ export function initVModelForSSR() {
return { checked: true }
}
}
+
+ vModelDynamic.getSSRProps = (binding, vnode) => {
+ if (typeof vnode.type !== 'string') {
+ return
+ }
+ const modelToUse = resolveDynamicModel(
+ // resolveDynamicModel expects an uppercase tag name, but vnode.type is lowercase
+ vnode.type.toUpperCase(),
+ vnode.props && vnode.props.type
+ )
+ if (modelToUse.getSSRProps) {
+ return modelToUse.getSSRProps(binding, vnode)
+ }
+ }
}
diff --git a/packages/runtime-dom/src/modules/events.ts b/packages/runtime-dom/src/modules/events.ts
index a4cb61e88..bd2279cf5 100644
--- a/packages/runtime-dom/src/modules/events.ts
+++ b/packages/runtime-dom/src/modules/events.ts
@@ -25,7 +25,7 @@ const [_getNow, skipTimestampCheck] = /*#__PURE__*/ (() => {
// if the low-res timestamp which is bigger than the event timestamp
// (which is evaluated AFTER) it means the event is using a hi-res timestamp,
// and we need to use the hi-res version for event listeners as well.
- _getNow = () => performance.now()
+ _getNow = performance.now.bind(performance)
}
// #3485: Firefox <= 53 has incorrect Event.timeStamp implementation
// and does not fire microtasks in between event propagation, so safe to exclude.
diff --git a/packages/runtime-dom/types/jsx.d.ts b/packages/runtime-dom/types/jsx.d.ts
index 7a00888f6..6120f7771 100644
--- a/packages/runtime-dom/types/jsx.d.ts
+++ b/packages/runtime-dom/types/jsx.d.ts
@@ -40,6 +40,7 @@ export interface CSSProperties
* For examples and more information, visit:
* https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
*/
+ [v: `--${string}`]: string | number | undefined
}
type Booleanish = boolean | 'true' | 'false'
@@ -457,7 +458,7 @@ export interface InputHTMLAttributes extends HTMLAttributes {
autocomplete?: string
autofocus?: Booleanish
capture?: boolean | 'user' | 'environment' // https://www.w3.org/tr/html-media-capture/#the-capture-attribute
- checked?: Booleanish | any[] // for IDE v-model multi-checkbox support
+ checked?: Booleanish | any[] | Set // for IDE v-model multi-checkbox support
crossorigin?: string
disabled?: Booleanish
form?: string
@@ -1309,10 +1310,7 @@ import * as RuntimeCore from '@vue/runtime-core'
type ReservedProps = {
key?: string | number | symbol
- ref?:
- | string
- | RuntimeCore.Ref
- | ((ref: Element | RuntimeCore.ComponentPublicInstance | null) => void)
+ ref?: RuntimeCore.VNodeRef
ref_for?: boolean
ref_key?: string
}
diff --git a/packages/runtime-test/package.json b/packages/runtime-test/package.json
index 282e0da0d..5dd2705b7 100644
--- a/packages/runtime-test/package.json
+++ b/packages/runtime-test/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/runtime-test",
- "version": "3.2.33",
+ "version": "3.2.37",
"description": "@vue/runtime-test",
"private": true,
"main": "index.js",
@@ -25,7 +25,7 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-test#readme",
"dependencies": {
- "@vue/shared": "3.2.33",
- "@vue/runtime-core": "3.2.33"
+ "@vue/shared": "3.2.37",
+ "@vue/runtime-core": "3.2.37"
}
}
diff --git a/packages/server-renderer/__tests__/render.spec.ts b/packages/server-renderer/__tests__/render.spec.ts
index 96e167509..4fd781fca 100644
--- a/packages/server-renderer/__tests__/render.spec.ts
+++ b/packages/server-renderer/__tests__/render.spec.ts
@@ -676,7 +676,7 @@ function testRender(type: string, render: typeof renderToString) {
render: () => h('p', 'hello')
}
expect(await render(h(KeepAlive, () => h(MyComp)))).toBe(
- `hello
`
+ `hello
`
)
})
diff --git a/packages/server-renderer/__tests__/ssrDirectives.spec.ts b/packages/server-renderer/__tests__/ssrDirectives.spec.ts
index 3e8bd2e0f..74b01204d 100644
--- a/packages/server-renderer/__tests__/ssrDirectives.spec.ts
+++ b/packages/server-renderer/__tests__/ssrDirectives.spec.ts
@@ -11,6 +11,7 @@ import {
vModelText,
vModelRadio,
vModelCheckbox,
+ vModelDynamic,
resolveDirective
} from 'vue'
import { ssrGetDirectiveProps, ssrRenderAttrs } from '../src'
@@ -376,6 +377,100 @@ describe('ssr: directives', () => {
})
})
+ describe('vnode v-model dynamic', () => {
+ test('text', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return withDirectives(h('input'), [[vModelDynamic, 'hello']])
+ }
+ })
+ )
+ ).toBe(``)
+ })
+
+ test('radio', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return withDirectives(
+ h('input', { type: 'radio', value: 'hello' }),
+ [[vModelDynamic, 'hello']]
+ )
+ }
+ })
+ )
+ ).toBe(``)
+
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return withDirectives(
+ h('input', { type: 'radio', value: 'hello' }),
+ [[vModelDynamic, 'foo']]
+ )
+ }
+ })
+ )
+ ).toBe(``)
+ })
+
+ test('checkbox', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return withDirectives(h('input', { type: 'checkbox' }), [
+ [vModelDynamic, true]
+ ])
+ }
+ })
+ )
+ ).toBe(``)
+
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return withDirectives(h('input', { type: 'checkbox' }), [
+ [vModelDynamic, false]
+ ])
+ }
+ })
+ )
+ ).toBe(``)
+
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return withDirectives(
+ h('input', { type: 'checkbox', value: 'foo' }),
+ [[vModelDynamic, ['foo']]]
+ )
+ }
+ })
+ )
+ ).toBe(``)
+
+ expect(
+ await renderToString(
+ createApp({
+ render() {
+ return withDirectives(
+ h('input', { type: 'checkbox', value: 'foo' }),
+ [[vModelDynamic, []]]
+ )
+ }
+ })
+ )
+ ).toBe(``)
+ })
+ })
+
test('custom directive w/ getSSRProps (vdom)', async () => {
expect(
await renderToString(
diff --git a/packages/server-renderer/__tests__/ssrSlot.spec.ts b/packages/server-renderer/__tests__/ssrSlot.spec.ts
new file mode 100644
index 000000000..9b93a55c0
--- /dev/null
+++ b/packages/server-renderer/__tests__/ssrSlot.spec.ts
@@ -0,0 +1,144 @@
+/**
+ * @jest-environment node
+ */
+
+import { createApp } from 'vue'
+import { renderToString } from '../src/renderToString'
+
+const components = {
+ one: {
+ template: `
`
+ }
+}
+
+describe('ssr: slot', () => {
+ test('text slot', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ components,
+ template: `hello`
+ })
+ )
+ ).toBe(`hello
`)
+ })
+
+ test('element slot', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ components,
+ template: `hi
`
+ })
+ )
+ ).toBe(``)
+ })
+
+ test('empty slot', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ components: {
+ one: {
+ template: `
`
+ }
+ },
+ template: ``
+ })
+ )
+ ).toBe(``)
+ })
+
+ test('empty slot (manual comments)', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ components: {
+ one: {
+ template: `
`
+ }
+ },
+ template: ``
+ })
+ )
+ ).toBe(``)
+ })
+
+ test('empty slot (multi-line comments)', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ components: {
+ one: {
+ template: `
`
+ }
+ },
+ template: ``
+ })
+ )
+ ).toBe(``)
+ })
+
+ test('multiple elements', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ components,
+ template: `one
two
`
+ })
+ )
+ ).toBe(``)
+ })
+
+ test('fragment slot (template v-if)', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ components,
+ template: `hello`
+ })
+ )
+ ).toBe(`hello
`)
+ })
+
+ test('fragment slot (template v-if + multiple elements)', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ components,
+ template: `one
two
`
+ })
+ )
+ ).toBe(
+ ``
+ )
+ })
+
+ test('transition slot', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ components: {
+ one: {
+ template: ``
+ }
+ },
+ template: `foo
`
+ })
+ )
+ ).toBe(``)
+
+ expect(
+ await renderToString(
+ createApp({
+ components: {
+ one: {
+ template: ``
+ }
+ },
+ template: `foo
`
+ })
+ )
+ ).toBe(`foo
`)
+ })
+})
diff --git a/packages/server-renderer/__tests__/ssrTeleport.spec.ts b/packages/server-renderer/__tests__/ssrTeleport.spec.ts
index f26c90358..76c5c8eb3 100644
--- a/packages/server-renderer/__tests__/ssrTeleport.spec.ts
+++ b/packages/server-renderer/__tests__/ssrTeleport.spec.ts
@@ -4,6 +4,7 @@
import { createApp, h, Teleport } from 'vue'
import { renderToString } from '../src/renderToString'
+import { renderToSimpleStream } from '../src/renderToStream'
import { SSRContext } from '../src/render'
import { ssrRenderTeleport } from '../src/helpers/ssrRenderTeleport'
@@ -30,7 +31,9 @@ describe('ssrRenderTeleport', () => {
ctx
)
expect(html).toBe('')
- expect(ctx.teleports!['#target']).toBe(`content
`)
+ expect(ctx.teleports!['#target']).toBe(
+ `content
`
+ )
})
test('teleport rendering (compiled + disabled)', async () => {
@@ -57,7 +60,7 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe(
'content
'
)
- expect(ctx.teleports!['#target']).toBe(``)
+ expect(ctx.teleports!['#target']).toBe(``)
})
test('teleport rendering (vnode)', async () => {
@@ -73,7 +76,9 @@ describe('ssrRenderTeleport', () => {
ctx
)
expect(html).toBe('')
- expect(ctx.teleports!['#target']).toBe('hello')
+ expect(ctx.teleports!['#target']).toBe(
+ 'hello'
+ )
})
test('teleport rendering (vnode + disabled)', async () => {
@@ -92,7 +97,7 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe(
'hello'
)
- expect(ctx.teleports!['#target']).toBe(``)
+ expect(ctx.teleports!['#target']).toBe(``)
})
test('multiple teleports with same target', async () => {
@@ -114,7 +119,7 @@ describe('ssrRenderTeleport', () => {
''
)
expect(ctx.teleports!['#target']).toBe(
- 'helloworld'
+ 'helloworld'
)
})
@@ -132,6 +137,43 @@ describe('ssrRenderTeleport', () => {
ctx
)
expect(html).toBe('')
- expect(ctx.teleports!['#target']).toBe(`content
`)
+ expect(ctx.teleports!['#target']).toBe(
+ `content
`
+ )
+ })
+
+ test('teleport inside async component (stream)', async () => {
+ const ctx: SSRContext = {}
+ const asyncComponent = {
+ template: 'content
',
+ async setup() {}
+ }
+ let html = ''
+ let resolve: any
+ const p = new Promise(r => (resolve = r))
+ renderToSimpleStream(
+ h({
+ template: '',
+ components: { asyncComponent }
+ }),
+ ctx,
+ {
+ push(chunk) {
+ if (chunk === null) {
+ resolve()
+ } else {
+ html += chunk
+ }
+ },
+ destroy(err) {
+ throw err
+ }
+ }
+ )
+ await p
+ expect(html).toBe('')
+ expect(ctx.teleports!['#target']).toBe(
+ `content
`
+ )
})
})
diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json
index 5c5b7f9fe..18b9a0a84 100644
--- a/packages/server-renderer/package.json
+++ b/packages/server-renderer/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/server-renderer",
- "version": "3.2.33",
+ "version": "3.2.37",
"description": "@vue/server-renderer",
"main": "index.js",
"module": "dist/server-renderer.esm-bundler.js",
@@ -13,6 +13,7 @@
"name": "VueServerRenderer",
"formats": [
"esm-bundler",
+ "esm-browser",
"cjs"
]
},
@@ -31,10 +32,10 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/server-renderer#readme",
"peerDependencies": {
- "vue": "3.2.33"
+ "vue": "3.2.37"
},
"dependencies": {
- "@vue/shared": "3.2.33",
- "@vue/compiler-ssr": "3.2.33"
+ "@vue/shared": "3.2.37",
+ "@vue/compiler-ssr": "3.2.37"
}
}
diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
index 967b20311..ea1e7e941 100644
--- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
@@ -40,7 +40,8 @@ export function ssrRenderSlotInner(
fallbackRenderFn: (() => void) | null,
push: PushFn,
parentComponent: ComponentInternalInstance,
- slotScopeId?: string
+ slotScopeId?: string,
+ transition?: boolean
) {
const slotFn = slots[slotName]
if (slotFn) {
@@ -61,10 +62,14 @@ export function ssrRenderSlotInner(
// ssr slot.
// check if the slot renders all comments, in which case use the fallback
let isEmptySlot = true
- for (let i = 0; i < slotBuffer.length; i++) {
- if (!isComment(slotBuffer[i])) {
- isEmptySlot = false
- break
+ if (transition) {
+ isEmptySlot = false
+ } else {
+ for (let i = 0; i < slotBuffer.length; i++) {
+ if (!isComment(slotBuffer[i])) {
+ isEmptySlot = false
+ break
+ }
}
}
if (isEmptySlot) {
@@ -82,7 +87,11 @@ export function ssrRenderSlotInner(
}
}
-const commentRE = /^$/
+const commentTestRE = /^$/s
+const commentRE = //gm
function isComment(item: SSRBufferItem) {
- return typeof item === 'string' && commentRE.test(item)
+ if (typeof item !== 'string' || !commentTestRE.test(item)) return false
+ // if item is '' or '' or '', return true directly
+ if (item.length <= 8) return true
+ return !item.replace(commentRE, '').trim()
}
diff --git a/packages/server-renderer/src/helpers/ssrRenderTeleport.ts b/packages/server-renderer/src/helpers/ssrRenderTeleport.ts
index 77331b7bd..8338ec06c 100644
--- a/packages/server-renderer/src/helpers/ssrRenderTeleport.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderTeleport.ts
@@ -10,28 +10,28 @@ export function ssrRenderTeleport(
) {
parentPush('')
- let teleportContent: SSRBufferItem
-
- if (disabled) {
- contentRenderFn(parentPush)
- teleportContent = ``
- } else {
- const { getBuffer, push } = createBuffer()
- contentRenderFn(push)
- push(``) // teleport end anchor
- teleportContent = getBuffer()
- }
-
const context = parentComponent.appContext.provides[
ssrContextKey as any
] as SSRContext
const teleportBuffers =
context.__teleportBuffers || (context.__teleportBuffers = {})
- if (teleportBuffers[target]) {
- teleportBuffers[target].push(teleportContent)
+ const targetBuffer = teleportBuffers[target] || (teleportBuffers[target] = [])
+ // record current index of the target buffer to handle nested teleports
+ // since the parent needs to be rendered before the child
+ const bufferIndex = targetBuffer.length
+
+ let teleportContent: SSRBufferItem
+
+ if (disabled) {
+ contentRenderFn(parentPush)
+ teleportContent = ``
} else {
- teleportBuffers[target] = [teleportContent]
+ const { getBuffer, push } = createBuffer()
+ contentRenderFn(push)
+ push(``)
+ teleportContent = getBuffer()
}
+ targetBuffer.splice(bufferIndex, 0, teleportContent)
parentPush('')
}
diff --git a/packages/server-renderer/src/render.ts b/packages/server-renderer/src/render.ts
index 4090ef9f4..ae71a9e62 100644
--- a/packages/server-renderer/src/render.ts
+++ b/packages/server-renderer/src/render.ts
@@ -4,6 +4,7 @@ import {
ComponentInternalInstance,
DirectiveBinding,
Fragment,
+ FunctionalComponent,
mergeProps,
ssrUtils,
Static,
@@ -112,12 +113,17 @@ function renderComponentSubTree(
const comp = instance.type as Component
const { getBuffer, push } = createBuffer()
if (isFunction(comp)) {
- renderVNode(
- push,
- (instance.subTree = renderComponentRoot(instance)),
- instance,
- slotScopeId
- )
+ let root = renderComponentRoot(instance)
+ // #5817 scope ID attrs not falling through if functional component doesn't
+ // have props
+ if (!(comp as FunctionalComponent).props) {
+ for (const key in instance.attrs) {
+ if (key.startsWith(`data-v-`)) {
+ ;(root.props || (root.props = {}))[key] = ``
+ }
+ }
+ }
+ renderVNode(push, (instance.subTree = root), instance, slotScopeId)
} else {
if (
(!instance.render || instance.render === NOOP) &&
diff --git a/packages/server-renderer/src/renderToStream.ts b/packages/server-renderer/src/renderToStream.ts
index 749fb54db..79484f9cf 100644
--- a/packages/server-renderer/src/renderToStream.ts
+++ b/packages/server-renderer/src/renderToStream.ts
@@ -9,6 +9,7 @@ import {
import { isString, isPromise } from '@vue/shared'
import { renderComponentVNode, SSRBuffer, SSRContext } from './render'
import { Readable, Writable } from 'stream'
+import { resolveTeleports } from './renderToString'
const { isVNode } = ssrUtils
@@ -74,6 +75,7 @@ export function renderToSimpleStream(
Promise.resolve(renderComponentVNode(vnode))
.then(buffer => unrollBuffer(buffer, stream))
+ .then(() => resolveTeleports(context))
.then(() => stream.push(null))
.catch(error => {
stream.destroy(error)
diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts
index f35ee9d62..2450b0a3a 100644
--- a/packages/server-renderer/src/renderToString.ts
+++ b/packages/server-renderer/src/renderToString.ts
@@ -70,7 +70,7 @@ export async function renderToString(
return result
}
-async function resolveTeleports(context: SSRContext) {
+export async function resolveTeleports(context: SSRContext) {
if (context.__teleportBuffers) {
context.teleports = context.teleports || {}
for (const key in context.__teleportBuffers) {
diff --git a/packages/sfc-playground/index.html b/packages/sfc-playground/index.html
index 77e3d3631..d9ca2eb80 100644
--- a/packages/sfc-playground/index.html
+++ b/packages/sfc-playground/index.html
@@ -9,10 +9,10 @@
-
+
history.replaceState({}, '', store.serialize()))
diff --git a/packages/sfc-playground/src/icons/Download.vue b/packages/sfc-playground/src/icons/Download.vue
index a7c4ed184..c5caec709 100644
--- a/packages/sfc-playground/src/icons/Download.vue
+++ b/packages/sfc-playground/src/icons/Download.vue
@@ -1,6 +1,6 @@
-
diff --git a/packages/sfc-playground/src/icons/Moon.vue b/packages/sfc-playground/src/icons/Moon.vue
index 2bfef28a9..30c6eb515 100644
--- a/packages/sfc-playground/src/icons/Moon.vue
+++ b/packages/sfc-playground/src/icons/Moon.vue
@@ -1,5 +1,5 @@
-
+
diff --git a/packages/sfc-playground/src/icons/Share.vue b/packages/sfc-playground/src/icons/Share.vue
index 829fcbe23..8dd9ccd84 100644
--- a/packages/sfc-playground/src/icons/Share.vue
+++ b/packages/sfc-playground/src/icons/Share.vue
@@ -2,7 +2,7 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+