,
): string[] {
switch (node.type) {
case 'StringLiteral':
return [node.value]
case 'TSLiteralType':
- return resolveStringType(ctx, node.literal, scope)
+ return resolveStringType(ctx, node.literal, scope, typeParameters)
case 'TSUnionType':
- return node.types.map(t => resolveStringType(ctx, t, scope)).flat()
+ return node.types
+ .map(t => resolveStringType(ctx, t, scope, typeParameters))
+ .flat()
case 'TemplateLiteral': {
return resolveTemplateKeys(ctx, node, scope)
}
case 'TSTypeReference': {
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
- return resolveStringType(ctx, resolved, scope)
+ return resolveStringType(ctx, resolved, scope, typeParameters)
}
if (node.typeName.type === 'Identifier') {
+ const name = node.typeName.name
+ if (typeParameters && typeParameters[name]) {
+ return resolveStringType(
+ ctx,
+ typeParameters[name],
+ scope,
+ typeParameters,
+ )
+ }
const getParam = (index = 0) =>
- resolveStringType(ctx, node.typeParameters!.params[index], scope)
- switch (node.typeName.name) {
+ resolveStringType(
+ ctx,
+ node.typeParameters!.params[index],
+ scope,
+ typeParameters,
+ )
+ switch (name) {
case 'Extract':
return getParam(1)
case 'Exclude': {
@@ -671,6 +688,7 @@ function resolveBuiltin(
ctx,
node.typeParameters!.params[1],
scope,
+ typeParameters,
)
const res: ResolvedElements = { props: {}, calls: t.calls }
for (const key of picked) {
@@ -683,6 +701,7 @@ function resolveBuiltin(
ctx,
node.typeParameters!.params[1],
scope,
+ typeParameters,
)
const res: ResolvedElements = { props: {}, calls: t.calls }
for (const key in t.props) {
@@ -860,13 +879,13 @@ function resolveFS(ctx: TypeResolveContext): FS | undefined {
}
return (ctx.fs = {
fileExists(file) {
- if (file.endsWith('.vue.ts')) {
+ if (file.endsWith('.vue.ts') && !file.endsWith('.d.vue.ts')) {
file = file.replace(/\.ts$/, '')
}
return fs.fileExists(file)
},
readFile(file) {
- if (file.endsWith('.vue.ts')) {
+ if (file.endsWith('.vue.ts') && !file.endsWith('.d.vue.ts')) {
file = file.replace(/\.ts$/, '')
}
return fs.readFile(file)
@@ -1059,7 +1078,7 @@ function resolveWithTS(
if (res.resolvedModule) {
let filename = res.resolvedModule.resolvedFileName
- if (filename.endsWith('.vue.ts')) {
+ if (filename.endsWith('.vue.ts') && !filename.endsWith('.d.vue.ts')) {
filename = filename.replace(/\.ts$/, '')
}
return fs.realpath ? fs.realpath(filename) : filename
@@ -1129,7 +1148,7 @@ export function fileToScope(
// fs should be guaranteed to exist here
const fs = resolveFS(ctx)!
const source = fs.readFile(filename) || ''
- const body = parseFile(filename, source, ctx.options.babelParserPlugins)
+ const body = parseFile(filename, source, fs, ctx.options.babelParserPlugins)
const scope = new TypeScope(filename, source, 0, recordImports(body))
recordTypes(ctx, body, scope, asGlobal)
fileToScopeCache.set(filename, scope)
@@ -1139,6 +1158,7 @@ export function fileToScope(
function parseFile(
filename: string,
content: string,
+ fs: FS,
parserPlugins?: SFCScriptCompileOptions['babelParserPlugins'],
): Statement[] {
const ext = extname(filename)
@@ -1151,7 +1171,21 @@ function parseFile(
),
sourceType: 'module',
}).program.body
- } else if (ext === '.vue') {
+ }
+
+ // simulate `allowArbitraryExtensions` on TypeScript >= 5.0
+ const isUnknownTypeSource = !/\.[cm]?[tj]sx?$/.test(filename)
+ const arbitraryTypeSource = `${filename.slice(0, -ext.length)}.d${ext}.ts`
+ const hasArbitraryTypeDeclaration =
+ isUnknownTypeSource && fs.fileExists(arbitraryTypeSource)
+ if (hasArbitraryTypeDeclaration) {
+ return babelParse(fs.readFile(arbitraryTypeSource)!, {
+ plugins: resolveParserPlugins('ts', parserPlugins, true),
+ sourceType: 'module',
+ }).program.body
+ }
+
+ if (ext === '.vue') {
const {
descriptor: { script, scriptSetup },
} = parse(content)
@@ -1554,6 +1588,15 @@ export function inferRuntimeType(
case 'TSTypeReference': {
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
+ // #13240
+ // Special case for function type aliases to ensure correct runtime behavior
+ // other type aliases still fallback to unknown as before
+ if (
+ resolved.type === 'TSTypeAliasDeclaration' &&
+ resolved.typeAnnotation.type === 'TSFunctionType'
+ ) {
+ return ['Function']
+ }
return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf)
}
diff --git a/packages/compiler-sfc/src/style/cssVars.ts b/packages/compiler-sfc/src/style/cssVars.ts
index 0397c7d79..c6d1633cf 100644
--- a/packages/compiler-sfc/src/style/cssVars.ts
+++ b/packages/compiler-sfc/src/style/cssVars.ts
@@ -23,7 +23,12 @@ export function genCssVarsFromList(
return `{\n ${vars
.map(
key =>
- `"${isSSR ? `--` : ``}${genVarName(id, key, isProd, isSSR)}": (${key})`,
+ // The `:` prefix here is used in `ssrRenderStyle` to distinguish whether
+ // a custom property comes from `ssrCssVars`. If it does, we need to reset
+ // its value to `initial` on the component instance to avoid unintentionally
+ // inheriting the same property value from a different instance of the same
+ // component in the outer scope.
+ `"${isSSR ? `:--` : ``}${genVarName(id, key, isProd, isSSR)}": (${key})`,
)
.join(',\n ')}\n}`
}
diff --git a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts
index 0bf7673d0..8a439dbf4 100644
--- a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts
@@ -166,6 +166,132 @@ describe('ssr: v-model', () => {
_push(\`\`)
}"
`)
+
+ expect(
+ compileWithWrapper(`
+ `).code,
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ }"
+ `)
+
+ expect(
+ compileWithWrapper(`
+ `).code,
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ }"
+ `)
+
+ expect(
+ compileWithWrapper(`
+ `).code,
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ }"
+ `)
+
+ expect(
+ compileWithWrapper(`
+ `).code,
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ }"
+ `)
})
test('', () => {
diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json
index bbc44dcb2..31681468f 100644
--- a/packages/compiler-ssr/package.json
+++ b/packages/compiler-ssr/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/compiler-ssr",
- "version": "3.5.14",
+ "version": "3.5.17",
"description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts",
diff --git a/packages/compiler-ssr/src/transforms/ssrVModel.ts b/packages/compiler-ssr/src/transforms/ssrVModel.ts
index 80e383931..cbe5b2b42 100644
--- a/packages/compiler-ssr/src/transforms/ssrVModel.ts
+++ b/packages/compiler-ssr/src/transforms/ssrVModel.ts
@@ -39,6 +39,18 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
}
}
+ const processSelectChildren = (children: TemplateChildNode[]) => {
+ children.forEach(child => {
+ if (child.type === NodeTypes.ELEMENT) {
+ processOption(child as PlainElementNode)
+ } else if (child.type === NodeTypes.FOR) {
+ processSelectChildren(child.children)
+ } else if (child.type === NodeTypes.IF) {
+ child.branches.forEach(b => processSelectChildren(b.children))
+ }
+ })
+ }
+
function processOption(plainNode: PlainElementNode) {
if (plainNode.tag === 'option') {
if (plainNode.props.findIndex(p => p.name === 'selected') === -1) {
@@ -65,9 +77,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
)
}
} else if (plainNode.tag === 'optgroup') {
- plainNode.children.forEach(option =>
- processOption(option as PlainElementNode),
- )
+ processSelectChildren(plainNode.children)
}
}
@@ -163,18 +173,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
checkDuplicatedValue()
node.children = [createInterpolation(model, model.loc)]
} else if (node.tag === 'select') {
- 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)
+ processSelectChildren(node.children)
} else {
context.onError(
createDOMCompilerError(
diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts
index a3ba6a39c..9b17f960b 100644
--- a/packages/reactivity/__tests__/reactive.spec.ts
+++ b/packages/reactivity/__tests__/reactive.spec.ts
@@ -195,8 +195,8 @@ describe('reactivity/reactive', () => {
test('toRaw on object using reactive as prototype', () => {
const original = { foo: 1 }
const observed = reactive(original)
- const inherted = Object.create(observed)
- expect(toRaw(inherted)).toBe(inherted)
+ const inherited = Object.create(observed)
+ expect(toRaw(inherited)).toBe(inherited)
})
test('toRaw on user Proxy wrapping reactive', () => {
diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts
index 9acd5c649..5c70b6082 100644
--- a/packages/reactivity/__tests__/readonly.spec.ts
+++ b/packages/reactivity/__tests__/readonly.spec.ts
@@ -8,7 +8,9 @@ import {
reactive,
readonly,
ref,
+ shallowRef,
toRaw,
+ triggerRef,
} from '../src'
/**
@@ -520,3 +522,16 @@ describe('reactivity/readonly', () => {
expect(r.value).toBe(ro)
})
})
+
+test.todo('should be able to trigger with triggerRef', () => {
+ const r = shallowRef({ a: 1 })
+ const ror = readonly(r)
+ let dummy
+ effect(() => {
+ dummy = ror.value.a
+ })
+ r.value.a = 2
+ expect(dummy).toBe(1)
+ triggerRef(ror)
+ expect(dummy).toBe(2)
+})
diff --git a/packages/reactivity/__tests__/watch.spec.ts b/packages/reactivity/__tests__/watch.spec.ts
index 245acfd63..9bec54e5f 100644
--- a/packages/reactivity/__tests__/watch.spec.ts
+++ b/packages/reactivity/__tests__/watch.spec.ts
@@ -277,4 +277,16 @@ describe('watch', () => {
expect(dummy).toEqual([1, 2, 3])
})
+
+ test('watch with immediate reset and sync flush', () => {
+ const value = ref(false)
+
+ watch(value, () => {
+ value.value = false
+ })
+
+ value.value = true
+ value.value = true
+ expect(value.value).toBe(false)
+ })
})
diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json
index 5f92c1afe..d861a5fb1 100644
--- a/packages/reactivity/package.json
+++ b/packages/reactivity/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/reactivity",
- "version": "3.5.14",
+ "version": "3.5.17",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",
diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts
index 184964c17..3fd81514b 100644
--- a/packages/reactivity/src/dep.ts
+++ b/packages/reactivity/src/dep.ts
@@ -15,6 +15,12 @@ class Dep implements Dependency {
_subs: Link | undefined = undefined
subsTail: Link | undefined = undefined
+ /**
+ * @internal
+ */
+ readonly __v_skip = true
+ // TODO isolatedDeclarations ReactiveFlags.SKIP
+
constructor(
private map: KeyToDepMap,
private key: unknown,
diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts
index 094bf226c..882916b16 100644
--- a/packages/reactivity/src/watch.ts
+++ b/packages/reactivity/src/watch.ts
@@ -260,11 +260,11 @@ export function watch(
: oldValue,
boundCleanup,
]
+ oldValue = newValue
call
? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
: // @ts-expect-error
cb!(...args)
- oldValue = newValue
} finally {
activeWatcher = currentWatcher
}
diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts
index 39032a636..ff06fbea7 100644
--- a/packages/runtime-core/__tests__/apiWatch.spec.ts
+++ b/packages/runtime-core/__tests__/apiWatch.spec.ts
@@ -1595,7 +1595,7 @@ describe('api: watch', () => {
num.value++
await nextTick()
- // would not be calld when value>1
+ // would not be called when value>1
expect(spy1).toHaveBeenCalledTimes(1)
expect(spy2).toHaveBeenCalledTimes(1)
})
@@ -1874,7 +1874,7 @@ describe('api: watch', () => {
expect(foo.value.a).toBe(2)
})
- test('watch immediate error in effect scope should be catched by onErrorCaptured', async () => {
+ test('watch immediate error in effect scope should be caught by onErrorCaptured', async () => {
const warn = vi.spyOn(console, 'warn')
warn.mockImplementation(() => {})
const ERROR_IN_SCOPE = 'ERROR_IN_SCOPE'
diff --git a/packages/runtime-core/__tests__/componentSlots.spec.ts b/packages/runtime-core/__tests__/componentSlots.spec.ts
index 2cf50b964..ad87e5367 100644
--- a/packages/runtime-core/__tests__/componentSlots.spec.ts
+++ b/packages/runtime-core/__tests__/componentSlots.spec.ts
@@ -6,6 +6,7 @@ import {
nodeOps,
ref,
render,
+ useSlots,
} from '@vue/runtime-test'
import { createBlock, normalizeVNode } from '../src/vnode'
import { createSlots } from '../src/helpers/createSlots'
@@ -42,6 +43,29 @@ describe('component: slots', () => {
expect(slots).toMatchObject({})
})
+ test('initSlots: ensure compiler marker non-enumerable', () => {
+ const Comp = {
+ render() {
+ const slots = useSlots()
+ // Only user-defined slots should be enumerable
+ expect(Object.keys(slots)).toEqual(['foo'])
+
+ // Internal compiler markers must still exist but be non-enumerable
+ expect(slots).toHaveProperty('_')
+ expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
+ false,
+ )
+ expect(slots).toHaveProperty('__')
+ expect(Object.getOwnPropertyDescriptor(slots, '__')!.enumerable).toBe(
+ false,
+ )
+ return h('div')
+ },
+ }
+ const slots = { foo: () => {}, _: 1, __: [1] }
+ render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
+ })
+
test('initSlots: should normalize object slots (when value is null, string, array)', () => {
const { slots } = renderWithSlots({
_inner: '_inner',
diff --git a/packages/runtime-core/__tests__/components/Teleport.spec.ts b/packages/runtime-core/__tests__/components/Teleport.spec.ts
index 4c35b1f2d..69a1c4cb2 100644
--- a/packages/runtime-core/__tests__/components/Teleport.spec.ts
+++ b/packages/runtime-core/__tests__/components/Teleport.spec.ts
@@ -16,6 +16,7 @@ import {
render,
serialize,
serializeInner,
+ useModel,
withDirectives,
} from '@vue/runtime-test'
import {
@@ -144,6 +145,62 @@ describe('renderer: teleport', () => {
`"Footer
"`,
)
})
+
+ // #13349
+ test('handle deferred teleport updates before and after mount', async () => {
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+
+ const show = ref(false)
+ const data2 = ref('2')
+ const data3 = ref('3')
+
+ const Comp = {
+ props: {
+ modelValue: {},
+ modelModifiers: {},
+ },
+ emits: ['update:modelValue'],
+ setup(props: any) {
+ const data2 = useModel(props, 'modelValue')
+ data2.value = '2+'
+ return () => h('span')
+ },
+ }
+
+ createDOMApp({
+ setup() {
+ setTimeout(() => (show.value = true), 5)
+ setTimeout(() => (data3.value = '3+'), 10)
+ },
+ render() {
+ return h(Fragment, null, [
+ h('span', { id: 'targetId001' }),
+ show.value
+ ? h(Fragment, null, [
+ h(Teleport, { to: '#targetId001', defer: true }, [
+ createTextVNode(String(data3.value)),
+ ]),
+ h(Comp, {
+ modelValue: data2.value,
+ 'onUpdate:modelValue': (event: any) =>
+ (data2.value = event),
+ }),
+ ])
+ : createCommentVNode('v-if'),
+ ])
+ },
+ }).mount(root)
+
+ expect(root.innerHTML).toMatchInlineSnapshot(
+ `""`,
+ )
+
+ await new Promise(r => setTimeout(r, 10))
+ expect(root.innerHTML).toMatchInlineSnapshot(
+ `"3+"`,
+ )
+ })
})
function runSharedTests(deferMode: boolean) {
diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index 20519cf99..4a9e0fac2 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -1654,6 +1654,29 @@ describe('SSR hydration', () => {
expect(`mismatch`).not.toHaveBeenWarned()
})
+ test('transition appear work with pre-existing class', () => {
+ const { vnode, container } = mountWithHydration(
+ `foo
`,
+ () =>
+ h(
+ Transition,
+ { appear: true },
+ {
+ default: () => h('div', { class: 'foo' }, 'foo'),
+ },
+ ),
+ )
+ expect(container.firstChild).toMatchInlineSnapshot(`
+
+ foo
+
+ `)
+ expect(vnode.el).toBe(container.firstChild)
+ expect(`mismatch`).not.toHaveBeenWarned()
+ })
+
test('transition appear with v-if', () => {
const show = false
const { vnode, container } = mountWithHydration(
diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
index 958c12748..1d8bb8420 100644
--- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
+++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
@@ -861,6 +861,114 @@ describe('renderer: optimized mode', () => {
expect(inner(root)).toBe('')
})
+ // #13305
+ test('patch Suspense nested in list nodes in optimized mode', async () => {
+ const deps: Promise[] = []
+
+ const Item = {
+ props: {
+ someId: { type: Number, required: true },
+ },
+ async setup(props: any) {
+ const p = new Promise(resolve => setTimeout(resolve, 1))
+ deps.push(p)
+
+ await p
+ return () => (
+ openBlock(),
+ createElementBlock('li', null, [
+ createElementVNode(
+ 'p',
+ null,
+ String(props.someId),
+ PatchFlags.TEXT,
+ ),
+ ])
+ )
+ },
+ }
+
+ const list = ref([1, 2, 3])
+ const App = {
+ setup() {
+ return () => (
+ openBlock(),
+ createElementBlock(
+ Fragment,
+ null,
+ [
+ createElementVNode(
+ 'p',
+ null,
+ JSON.stringify(list.value),
+ PatchFlags.TEXT,
+ ),
+ createElementVNode('ol', null, [
+ (openBlock(),
+ createBlock(SuspenseImpl, null, {
+ fallback: withCtx(() => [
+ createElementVNode('li', null, 'Loading…'),
+ ]),
+ default: withCtx(() => [
+ (openBlock(true),
+ createElementBlock(
+ Fragment,
+ null,
+ renderList(list.value, id => {
+ return (
+ openBlock(),
+ createBlock(
+ Item,
+ {
+ key: id,
+ 'some-id': id,
+ },
+ null,
+ PatchFlags.PROPS,
+ ['some-id'],
+ )
+ )
+ }),
+ PatchFlags.KEYED_FRAGMENT,
+ )),
+ ]),
+ _: 1 /* STABLE */,
+ })),
+ ]),
+ ],
+ PatchFlags.STABLE_FRAGMENT,
+ )
+ )
+ },
+ }
+
+ const app = createApp(App)
+ app.mount(root)
+ expect(inner(root)).toBe(`[1,2,3]
` + `- Loading…
`)
+
+ await Promise.all(deps)
+ await nextTick()
+ expect(inner(root)).toBe(
+ `[1,2,3]
` +
+ `` +
+ `1
` +
+ `2
` +
+ `3
` +
+ `
`,
+ )
+
+ list.value = [3, 1, 2]
+ await nextTick()
+ expect(inner(root)).toBe(
+ `[3,1,2]
` +
+ `` +
+ `3
` +
+ `1
` +
+ `2
` +
+ `
`,
+ )
+ })
+
// #4183
test('should not take unmount children fast path /w Suspense', async () => {
const show = ref(true)
diff --git a/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts b/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts
index a7ae7a06b..7803826e3 100644
--- a/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts
+++ b/packages/runtime-core/__tests__/rendererTemplateRef.spec.ts
@@ -179,6 +179,37 @@ describe('api: template refs', () => {
expect(el.value).toBe(null)
})
+ it('unset old ref when new ref is absent', async () => {
+ const root1 = nodeOps.createElement('div')
+ const root2 = nodeOps.createElement('div')
+ const el1 = ref(null)
+ const el2 = ref(null)
+ const toggle = ref(true)
+
+ const Comp1 = {
+ setup() {
+ return () => (toggle.value ? h('div', { ref: el1 }) : h('div'))
+ },
+ }
+
+ const Comp2 = {
+ setup() {
+ return () => h('div', { ref: toggle.value ? el2 : undefined })
+ },
+ }
+
+ render(h(Comp1), root1)
+ render(h(Comp2), root2)
+
+ expect(el1.value).toBe(root1.children[0])
+ expect(el2.value).toBe(root2.children[0])
+
+ toggle.value = false
+ await nextTick()
+ expect(el1.value).toBe(null)
+ expect(el2.value).toBe(null)
+ })
+
test('string ref inside slots', async () => {
const root = nodeOps.createElement('div')
const spy = vi.fn()
diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json
index eea03cca6..1e60e27cb 100644
--- a/packages/runtime-core/package.json
+++ b/packages/runtime-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/runtime-core",
- "version": "3.5.14",
+ "version": "3.5.17",
"description": "@vue/runtime-core",
"main": "index.js",
"module": "dist/runtime-core.esm-bundler.js",
diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts
index 199b451f6..cb675f06e 100644
--- a/packages/runtime-core/src/apiAsyncComponent.ts
+++ b/packages/runtime-core/src/apiAsyncComponent.ts
@@ -4,6 +4,7 @@ import {
type ComponentOptions,
type ConcreteComponent,
currentInstance,
+ getComponentName,
isInSSRComponentSetup,
} from './component'
import { isFunction, isObject } from '@vue/shared'
@@ -121,14 +122,27 @@ export function defineAsyncComponent<
__asyncLoader: load,
__asyncHydrate(el, instance, hydrate) {
+ let patched = false
const doHydrate = hydrateStrategy
? () => {
- const teardown = hydrateStrategy(hydrate, cb =>
+ const performHydrate = () => {
+ // skip hydration if the component has been patched
+ if (__DEV__ && patched) {
+ warn(
+ `Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` +
+ `it was updated before lazy hydration performed.`,
+ )
+ return
+ }
+ hydrate()
+ }
+ const teardown = hydrateStrategy(performHydrate, cb =>
forEachElement(el, cb),
)
if (teardown) {
;(instance.bum || (instance.bum = [])).push(teardown)
}
+ ;(instance.u || (instance.u = [])).push(() => (patched = true))
}
: hydrate
if (resolvedComp) {
diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts
index e79b46d46..d26a14b74 100644
--- a/packages/runtime-core/src/apiCreateApp.ts
+++ b/packages/runtime-core/src/apiCreateApp.ts
@@ -22,7 +22,7 @@ import { warn } from './warning'
import { type VNode, cloneVNode, createVNode } from './vnode'
import type { RootHydrateFunction } from './hydration'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
-import { NO, extend, isFunction, isObject } from '@vue/shared'
+import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared'
import { version } from '.'
import { installAppCompatProperties } from './compat/global'
import type { NormalizedPropsOptions } from './componentProps'
@@ -442,10 +442,18 @@ export function createAppAPI(
provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
- warn(
- `App already provides property with key "${String(key)}". ` +
- `It will be overwritten with the new value.`,
- )
+ if (hasOwn(context.provides, key as string | symbol)) {
+ warn(
+ `App already provides property with key "${String(key)}". ` +
+ `It will be overwritten with the new value.`,
+ )
+ } else {
+ // #13212, context.provides can inherit the provides object from parent on custom elements
+ warn(
+ `App already provides property with key "${String(key)}" inherited from its parent element. ` +
+ `It will be overwritten with the new value.`,
+ )
+ }
}
context.provides[key as string | symbol] = value
diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts
index f05d7333d..711c5d84d 100644
--- a/packages/runtime-core/src/apiInject.ts
+++ b/packages/runtime-core/src/apiInject.ts
@@ -59,10 +59,12 @@ export function inject(
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
// #11488, in a nested createApp, prioritize using the provides from currentApp
- const provides = currentApp
+ // #13212, for custom elements we must get injected values from its appContext
+ // as it already inherits the provides object from the parent element
+ let provides = currentApp
? currentApp._context.provides
: instance
- ? instance.parent == null
+ ? instance.parent == null || instance.ce
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: undefined
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 5b094a0d6..60552d736 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -115,20 +115,23 @@ export type ComponentInstance = T extends { new (): ComponentPublicInstance }
: T extends FunctionalComponent
? ComponentPublicInstance>
: T extends Component<
- infer Props,
+ infer PropsOrInstance,
infer RawBindings,
infer D,
infer C,
infer M
>
- ? // NOTE we override Props/RawBindings/D to make sure is not `unknown`
- ComponentPublicInstance<
- unknown extends Props ? {} : Props,
- unknown extends RawBindings ? {} : RawBindings,
- unknown extends D ? {} : D,
- C,
- M
- >
+ ? PropsOrInstance extends { $props: unknown }
+ ? // T is returned by `defineComponent()`
+ PropsOrInstance
+ : // NOTE we override Props/RawBindings/D to make sure is not `unknown`
+ ComponentPublicInstance<
+ unknown extends PropsOrInstance ? {} : PropsOrInstance,
+ unknown extends RawBindings ? {} : RawBindings,
+ unknown extends D ? {} : D,
+ C,
+ M
+ >
: never // not a vue Component
/**
@@ -259,7 +262,7 @@ export type ConcreteComponent<
* The constructor type is an artificial type returned by defineComponent().
*/
export type Component<
- Props = any,
+ PropsOrInstance = any,
RawBindings = any,
D = any,
C extends ComputedOptions = ComputedOptions,
@@ -267,8 +270,8 @@ export type Component<
E extends EmitsOptions | Record = {},
S extends Record = any,
> =
- | ConcreteComponent
- | ComponentPublicInstanceConstructor
+ | ConcreteComponent
+ | ComponentPublicInstanceConstructor
export type { ComponentOptions }
@@ -582,13 +585,13 @@ export interface ComponentInternalInstance {
* For updating css vars on contained teleports
* @internal
*/
- ut?: (vars?: Record) => void
+ ut?: (vars?: Record) => void
/**
* dev only. For style v-bind hydration mismatch checks
* @internal
*/
- getCssVars?: () => Record
+ getCssVars?: () => Record
/**
* v2 compat only, for caching mutated $options
diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts
index db52bc88c..c03bead3a 100644
--- a/packages/runtime-core/src/componentEmits.ts
+++ b/packages/runtime-core/src/componentEmits.ts
@@ -151,10 +151,14 @@ export function emit(
}
let args = rawArgs
- const isModelListener = event.startsWith('update:')
+ const isCompatModelListener =
+ __COMPAT__ && compatModelEventPrefix + event in props
+ const isModelListener = isCompatModelListener || event.startsWith('update:')
+ const modifiers = isCompatModelListener
+ ? props.modelModifiers
+ : isModelListener && getModelModifiers(props, event.slice(7))
// for v-model update:xxx events, apply modifiers on args
- const modifiers = isModelListener && getModelModifiers(props, event.slice(7))
if (modifiers) {
if (modifiers.trim) {
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts
index 8baa78086..775eb8b67 100644
--- a/packages/runtime-core/src/componentProps.ts
+++ b/packages/runtime-core/src/componentProps.ts
@@ -143,7 +143,9 @@ type InferPropType = [T] extends [null]
export type ExtractPropTypes = {
// use `keyof Pick>` instead of `RequiredKeys` to
// support IDE features
- [K in keyof Pick>]: InferPropType
+ [K in keyof Pick>]: O[K] extends { default: any }
+ ? Exclude, undefined>
+ : InferPropType
} & {
// use `keyof Pick>` instead of `OptionalKeys` to
// support IDE features
diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts
index 381269543..6114f6c86 100644
--- a/packages/runtime-core/src/componentSlots.ts
+++ b/packages/runtime-core/src/componentSlots.ts
@@ -193,6 +193,10 @@ export const initSlots = (
): void => {
const slots = (instance.slots = createInternalObject())
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
+ const cacheIndexes = (children as RawSlots).__
+ // make cache indexes marker non-enumerable
+ if (cacheIndexes) def(slots, '__', cacheIndexes, true)
+
const type = (children as RawSlots)._
if (type) {
assignSlots(slots, children as Slots, optimized)
diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts
index fc2ee4c08..c37356a78 100644
--- a/packages/runtime-core/src/components/Teleport.ts
+++ b/packages/runtime-core/src/components/Teleport.ts
@@ -164,15 +164,16 @@ export const TeleportImpl = {
}
if (isTeleportDeferred(n2.props)) {
+ n2.el!.__isMounted = false
queuePostRenderEffect(() => {
mountToTarget()
- n2.el!.__isMounted = true
+ delete n2.el!.__isMounted
}, parentSuspense)
} else {
mountToTarget()
}
} else {
- if (isTeleportDeferred(n2.props) && !n1.el!.__isMounted) {
+ if (isTeleportDeferred(n2.props) && n1.el!.__isMounted === false) {
queuePostRenderEffect(() => {
TeleportImpl.process(
n1,
@@ -186,7 +187,6 @@ export const TeleportImpl = {
optimized,
internals,
)
- delete n1.el!.__isMounted
}, parentSuspense)
return
}
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index a94ff3568..bdebed596 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -28,6 +28,7 @@ import {
isReservedProp,
isString,
normalizeClass,
+ normalizeCssVarValue,
normalizeStyle,
stringifyStyle,
} from '@vue/shared'
@@ -398,9 +399,11 @@ export function createHydrationFunctions(
parentComponent.vnode.props.appear
const content = (el as HTMLTemplateElement).content
- .firstChild as Element
+ .firstChild as Element & { $cls?: string }
if (needCallTransitionHooks) {
+ const cls = content.getAttribute('class')
+ if (cls) content.$cls = cls
transition!.beforeEnter(content)
}
@@ -786,7 +789,7 @@ export function createHydrationFunctions(
* Dev only
*/
function propHasMismatch(
- el: Element,
+ el: Element & { $cls?: string },
key: string,
clientValue: any,
vnode: VNode,
@@ -799,7 +802,12 @@ function propHasMismatch(
if (key === 'class') {
// classes might be in different order, but that doesn't affect cascade
// so we just need to check if the class lists contain the same classes.
- actual = el.getAttribute('class')
+ if (el.$cls) {
+ actual = el.$cls
+ delete el.$cls
+ } else {
+ actual = el.getAttribute('class')
+ }
expected = normalizeClass(clientValue)
if (!isSetEqual(toClassSet(actual || ''), toClassSet(expected))) {
mismatchType = MismatchTypes.CLASS
@@ -938,10 +946,8 @@ function resolveCssVars(
) {
const cssVars = instance.getCssVars()
for (const key in cssVars) {
- expectedMap.set(
- `--${getEscapedCssVarName(key, false)}`,
- String(cssVars[key]),
- )
+ const value = normalizeCssVarValue(cssVars[key])
+ expectedMap.set(`--${getEscapedCssVarName(key, false)}`, value)
}
}
if (vnode === root && instance.parent) {
@@ -990,6 +996,6 @@ function isMismatchAllowed(
if (allowedType === MismatchTypes.TEXT && list.includes('children')) {
return true
}
- return allowedAttr.split(',').includes(MismatchTypeString[allowedType])
+ return list.includes(MismatchTypeString[allowedType])
}
}
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 7b73d0f77..3550a2aef 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -86,6 +86,7 @@ import { isAsyncWrapper } from './apiAsyncComponent'
import { isCompatEnabled } from './compat/compatConfig'
import { DeprecationTypes } from './compat/compatConfig'
import type { TransitionHooks } from './components/BaseTransition'
+import type { VueElement } from '@vue/runtime-dom'
export interface Renderer {
render: RootRenderFunction
@@ -484,6 +485,8 @@ function baseCreateRenderer(
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
+ } else if (ref == null && n1 && n1.ref != null) {
+ setRef(n1.ref, null, parentSuspense, n1, true)
}
}
@@ -961,7 +964,8 @@ function baseCreateRenderer(
// which also requires the correct parent container
!isSameVNodeType(oldVNode, newVNode) ||
// - In the case of a component, it could contain anything.
- oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
+ oldVNode.shapeFlag &
+ (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT | ShapeFlags.SUSPENSE))
? hostParentNode(oldVNode.el)!
: // In other cases, the parent container is not actually used so we
// just pass the block element here to avoid a DOM parentNode call.
@@ -1347,7 +1351,11 @@ function baseCreateRenderer(
}
} else {
// custom element style injection
- if (root.ce) {
+ if (
+ root.ce &&
+ // @ts-expect-error _def is private
+ (root.ce as VueElement)._def.shadowRoot !== false
+ ) {
root.ce._injectChildStyle(type)
}
diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts
index df438d47e..c44840df5 100644
--- a/packages/runtime-dom/__tests__/customElement.spec.ts
+++ b/packages/runtime-dom/__tests__/customElement.spec.ts
@@ -444,6 +444,36 @@ describe('defineCustomElement', () => {
const e = container.childNodes[0] as VueElement
expect(e.shadowRoot!.innerHTML).toBe('hello')
})
+
+ test('prop types validation', async () => {
+ const E = defineCustomElement({
+ props: {
+ num: {
+ type: [Number, String],
+ },
+ bool: {
+ type: Boolean,
+ },
+ },
+ render() {
+ return h('div', [
+ h('span', [`${this.num} is ${typeof this.num}`]),
+ h('span', [`${this.bool} is ${typeof this.bool}`]),
+ ])
+ },
+ })
+
+ customElements.define('my-el-with-type-props', E)
+ render(h('my-el-with-type-props', { num: 1, bool: true }), container)
+ const e = container.childNodes[0] as VueElement
+ // @ts-expect-error
+ expect(e.num).toBe(1)
+ // @ts-expect-error
+ expect(e.bool).toBe(true)
+ expect(e.shadowRoot!.innerHTML).toBe(
+ '1 is numbertrue is boolean
',
+ )
+ })
})
describe('attrs', () => {
@@ -708,6 +738,101 @@ describe('defineCustomElement', () => {
`changedA! changedB!
`,
)
})
+
+ // #13212
+ test('inherited from app context within nested elements', async () => {
+ const outerValues: (string | undefined)[] = []
+ const innerValues: (string | undefined)[] = []
+ const innerChildValues: (string | undefined)[] = []
+
+ const Outer = defineCustomElement(
+ {
+ setup() {
+ outerValues.push(
+ inject('shared'),
+ inject('outer'),
+ inject('inner'),
+ )
+ },
+ render() {
+ return h('div', [renderSlot(this.$slots, 'default')])
+ },
+ },
+ {
+ configureApp(app) {
+ app.provide('shared', 'shared')
+ app.provide('outer', 'outer')
+ },
+ },
+ )
+
+ const Inner = defineCustomElement(
+ {
+ setup() {
+ // ensure values are not self-injected
+ provide('inner', 'inner-child')
+
+ innerValues.push(
+ inject('shared'),
+ inject('outer'),
+ inject('inner'),
+ )
+ },
+ render() {
+ return h('div', [renderSlot(this.$slots, 'default')])
+ },
+ },
+ {
+ configureApp(app) {
+ app.provide('outer', 'override-outer')
+ app.provide('inner', 'inner')
+ },
+ },
+ )
+
+ const InnerChild = defineCustomElement({
+ setup() {
+ innerChildValues.push(
+ inject('shared'),
+ inject('outer'),
+ inject('inner'),
+ )
+ },
+ render() {
+ return h('div')
+ },
+ })
+
+ customElements.define('provide-from-app-outer', Outer)
+ customElements.define('provide-from-app-inner', Inner)
+ customElements.define('provide-from-app-inner-child', InnerChild)
+
+ container.innerHTML =
+ '' +
+ '' +
+ '' +
+ '' +
+ ''
+
+ const outer = container.childNodes[0] as VueElement
+ expect(outer.shadowRoot!.innerHTML).toBe('
')
+
+ expect('[Vue warn]: injection "inner" not found.').toHaveBeenWarnedTimes(
+ 1,
+ )
+ expect(
+ '[Vue warn]: App already provides property with key "outer" inherited from its parent element. ' +
+ 'It will be overwritten with the new value.',
+ ).toHaveBeenWarnedTimes(1)
+
+ expect(outerValues).toEqual(['shared', 'outer', undefined])
+ expect(innerValues).toEqual(['shared', 'override-outer', 'inner'])
+ expect(innerChildValues).toEqual([
+ 'shared',
+ 'override-outer',
+ 'inner-child',
+ ])
+ })
})
describe('styles', () => {
@@ -791,6 +916,30 @@ describe('defineCustomElement', () => {
assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
})
+ test("child components should not inject styles to root element's shadow root w/ shadowRoot false", async () => {
+ const Bar = defineComponent({
+ styles: [`div { color: green; }`],
+ render() {
+ return 'bar'
+ },
+ })
+ const Baz = () => h(Bar)
+ const Foo = defineCustomElement(
+ {
+ render() {
+ return [h(Baz)]
+ },
+ },
+ { shadowRoot: false },
+ )
+
+ customElements.define('my-foo-with-shadowroot-false', Foo)
+ container.innerHTML = ``
+ const el = container.childNodes[0] as VueElement
+ const style = el.shadowRoot?.querySelector('style')
+ expect(style).toBeUndefined()
+ })
+
test('with nonce', () => {
const Foo = defineCustomElement(
{
@@ -1131,6 +1280,92 @@ describe('defineCustomElement', () => {
expect(target.innerHTML).toBe(`default`)
app.unmount()
})
+
+ test('toggle nested custom element with shadowRoot: false', async () => {
+ customElements.define(
+ 'my-el-child-shadow-false',
+ defineCustomElement(
+ {
+ render(ctx: any) {
+ return h('div', null, [renderSlot(ctx.$slots, 'default')])
+ },
+ },
+ { shadowRoot: false },
+ ),
+ )
+ const ChildWrapper = {
+ render() {
+ return h('my-el-child-shadow-false', null, 'child')
+ },
+ }
+
+ customElements.define(
+ 'my-el-parent-shadow-false',
+ defineCustomElement(
+ {
+ props: {
+ isShown: { type: Boolean, required: true },
+ },
+ render(ctx: any, _: any, $props: any) {
+ return $props.isShown
+ ? h('div', { key: 0 }, [renderSlot(ctx.$slots, 'default')])
+ : null
+ },
+ },
+ { shadowRoot: false },
+ ),
+ )
+ const ParentWrapper = {
+ props: {
+ isShown: { type: Boolean, required: true },
+ },
+ render(ctx: any, _: any, $props: any) {
+ return h('my-el-parent-shadow-false', { isShown: $props.isShown }, [
+ renderSlot(ctx.$slots, 'default'),
+ ])
+ },
+ }
+
+ const isShown = ref(true)
+ const App = {
+ render() {
+ return h(ParentWrapper, { isShown: isShown.value } as any, {
+ default: () => [h(ChildWrapper)],
+ })
+ },
+ }
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ const app = createApp(App)
+ app.mount(container)
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `
` +
+ `child
` +
+ `` +
+ `
` +
+ ``,
+ )
+
+ isShown.value = false
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ ``,
+ )
+
+ isShown.value = true
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `
` +
+ `child
` +
+ `` +
+ `
` +
+ ``,
+ )
+ })
})
describe('helpers', () => {
@@ -1330,6 +1565,64 @@ describe('defineCustomElement', () => {
expect(e.shadowRoot?.innerHTML).toBe('app-injected
')
})
+
+ // #12448
+ test('work with async component', async () => {
+ const AsyncComp = defineAsyncComponent(() => {
+ return Promise.resolve({
+ render() {
+ const msg: string | undefined = inject('msg')
+ return h('div', {}, msg)
+ },
+ } as any)
+ })
+ const E = defineCustomElement(AsyncComp, {
+ configureApp(app) {
+ app.provide('msg', 'app-injected')
+ },
+ })
+ customElements.define('my-async-element-with-app', E)
+
+ container.innerHTML = ``
+ const e = container.childNodes[0] as VueElement
+ await new Promise(r => setTimeout(r))
+ expect(e.shadowRoot?.innerHTML).toBe('app-injected
')
+ })
+
+ test('with hmr reload', async () => {
+ const __hmrId = '__hmrWithApp'
+ const def = defineComponent({
+ __hmrId,
+ setup() {
+ const msg = inject('msg')
+ return { msg }
+ },
+ render(this: any) {
+ return h('div', [h('span', this.msg), h('span', this.$foo)])
+ },
+ })
+ const E = defineCustomElement(def, {
+ configureApp(app) {
+ app.provide('msg', 'app-injected')
+ app.config.globalProperties.$foo = 'foo'
+ },
+ })
+ customElements.define('my-element-with-app-hmr', E)
+
+ container.innerHTML = ``
+ const el = container.childNodes[0] as VueElement
+ expect(el.shadowRoot?.innerHTML).toBe(
+ `app-injectedfoo
`,
+ )
+
+ // hmr
+ __VUE_HMR_RUNTIME__.reload(__hmrId, def as any)
+
+ await nextTick()
+ expect(el.shadowRoot?.innerHTML).toBe(
+ `app-injectedfoo
`,
+ )
+ })
})
// #9885
diff --git a/packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts b/packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts
index 1fb4cc65f..e2102e0c7 100644
--- a/packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts
+++ b/packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts
@@ -465,4 +465,27 @@ describe('useCssVars', () => {
render(h(App), root)
expect(colorInOnMount).toBe(`red`)
})
+
+ test('should set vars as `initial` for nullish values', async () => {
+ // `getPropertyValue` cannot reflect the real value for white spaces and JSDOM also
+ // doesn't 100% reflect the real behavior of browsers, so we only keep the test for
+ // `initial` value here.
+ // The value normalization is tested in packages/shared/__tests__/cssVars.spec.ts.
+ const state = reactive>({
+ foo: undefined,
+ bar: null,
+ })
+ const root = document.createElement('div')
+ const App = {
+ setup() {
+ useCssVars(() => state)
+ return () => h('div')
+ },
+ }
+ render(h(App), root)
+ await nextTick()
+ const style = (root.children[0] as HTMLElement).style
+ expect(style.getPropertyValue('--foo')).toBe('initial')
+ expect(style.getPropertyValue('--bar')).toBe('initial')
+ })
})
diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json
index 54fce7d66..4d71206e2 100644
--- a/packages/runtime-dom/package.json
+++ b/packages/runtime-dom/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/runtime-dom",
- "version": "3.5.14",
+ "version": "3.5.17",
"description": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",
diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts
index aeeaeec9b..edf7c4313 100644
--- a/packages/runtime-dom/src/apiCustomElement.ts
+++ b/packages/runtime-dom/src/apiCustomElement.ts
@@ -269,18 +269,14 @@ export class VueElement
this._root = this
}
}
-
- if (!(this._def as ComponentOptions).__asyncLoader) {
- // for sync component defs we can immediately resolve props
- this._resolveProps(this._def)
- }
}
connectedCallback(): void {
// avoid resolving component if it's not connected
if (!this.isConnected) return
- if (!this.shadowRoot) {
+ // avoid re-parsing slots if already resolved
+ if (!this.shadowRoot && !this._resolved) {
this._parseSlots()
}
this._connected = true
@@ -298,8 +294,7 @@ export class VueElement
if (!this._instance) {
if (this._resolved) {
- this._setParent()
- this._update()
+ this._mount(this._def)
} else {
if (parent && parent._pendingResolve) {
this._pendingResolve = parent._pendingResolve.then(() => {
@@ -316,7 +311,18 @@ export class VueElement
private _setParent(parent = this._parent) {
if (parent) {
this._instance!.parent = parent._instance
- this._instance!.provides = parent._instance!.provides
+ this._inheritParentContext(parent)
+ }
+ }
+
+ private _inheritParentContext(parent = this._parent) {
+ // #13212, the provides object of the app context must inherit the provides
+ // object from the parent element so we can inject values from both places
+ if (parent && this._app) {
+ Object.setPrototypeOf(
+ this._app._context.provides,
+ parent._instance!.provides,
+ )
}
}
@@ -380,12 +386,7 @@ export class VueElement
}
}
this._numberProps = numberProps
-
- if (isAsync) {
- // defining getter/setters on prototype
- // for sync defs, this already happened in the constructor
- this._resolveProps(def)
- }
+ this._resolveProps(def)
// apply CSS
if (this.shadowRoot) {
@@ -403,9 +404,10 @@ export class VueElement
const asyncDef = (this._def as ComponentOptions).__asyncLoader
if (asyncDef) {
- this._pendingResolve = asyncDef().then(def =>
- resolve((this._def = def), true),
- )
+ this._pendingResolve = asyncDef().then((def: InnerComponentDef) => {
+ def.configureApp = this._def.configureApp
+ resolve((this._def = def), true)
+ })
} else {
resolve(this._def)
}
@@ -417,6 +419,8 @@ export class VueElement
def.name = 'VueElement'
}
this._app = this._createApp(def)
+ // inherit before configureApp to detect context overwrites
+ this._inheritParentContext()
if (def.configureApp) {
def.configureApp(this._app)
}
@@ -520,7 +524,9 @@ export class VueElement
}
private _update() {
- render(this._createVNode(), this._root)
+ const vnode = this._createVNode()
+ if (this._app) vnode.appContext = this._app._context
+ render(vnode, this._root)
}
private _createVNode(): VNode {
diff --git a/packages/runtime-dom/src/helpers/useCssVars.ts b/packages/runtime-dom/src/helpers/useCssVars.ts
index e2bc6de92..3032143d9 100644
--- a/packages/runtime-dom/src/helpers/useCssVars.ts
+++ b/packages/runtime-dom/src/helpers/useCssVars.ts
@@ -10,14 +10,16 @@ import {
warn,
watch,
} from '@vue/runtime-core'
-import { NOOP, ShapeFlags } from '@vue/shared'
+import { NOOP, ShapeFlags, normalizeCssVarValue } from '@vue/shared'
export const CSS_VAR_TEXT: unique symbol = Symbol(__DEV__ ? 'CSS_VAR_TEXT' : '')
/**
* Runtime helper for SFC's CSS variable injection feature.
* @private
*/
-export function useCssVars(getter: (ctx: any) => Record): void {
+export function useCssVars(
+ getter: (ctx: any) => Record,
+): void {
if (!__BROWSER__ && !__TEST__) return
const instance = getCurrentInstance()
@@ -64,7 +66,7 @@ export function useCssVars(getter: (ctx: any) => Record): void {
})
}
-function setVarsOnVNode(vnode: VNode, vars: Record) {
+function setVarsOnVNode(vnode: VNode, vars: Record) {
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
const suspense = vnode.suspense!
vnode = suspense.activeBranch!
@@ -94,13 +96,14 @@ function setVarsOnVNode(vnode: VNode, vars: Record) {
}
}
-function setVarsOnNode(el: Node, vars: Record) {
+function setVarsOnNode(el: Node, vars: Record) {
if (el.nodeType === 1) {
const style = (el as HTMLElement).style
let cssText = ''
for (const key in vars) {
- style.setProperty(`--${key}`, vars[key])
- cssText += `--${key}: ${vars[key]};`
+ const value = normalizeCssVarValue(vars[key])
+ style.setProperty(`--${key}`, value)
+ cssText += `--${key}: ${value};`
}
;(style as any)[CSS_VAR_TEXT] = cssText
}
diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts
index ca9a307dd..c69375983 100644
--- a/packages/runtime-dom/src/index.ts
+++ b/packages/runtime-dom/src/index.ts
@@ -58,8 +58,8 @@ declare module '@vue/runtime-core' {
vOn: VOnDirective
vBind: VModelDirective
vIf: Directive
- VOnce: Directive
- VSlot: Directive
+ vOnce: Directive
+ vSlot: Directive
}
}
diff --git a/packages/runtime-dom/src/modules/attrs.ts b/packages/runtime-dom/src/modules/attrs.ts
index 95e0a1485..d7188aa9a 100644
--- a/packages/runtime-dom/src/modules/attrs.ts
+++ b/packages/runtime-dom/src/modules/attrs.ts
@@ -79,6 +79,7 @@ export function compatCoerceAttr(
}
} else if (
value === false &&
+ !(el.tagName === 'INPUT' && key === 'value') &&
!isSpecialBooleanAttr(key) &&
compatUtils.isCompatEnabled(DeprecationTypes.ATTR_FALSE_VALUE, instance)
) {
diff --git a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts
index 9f33866e5..984387bb8 100644
--- a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts
+++ b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts
@@ -203,4 +203,19 @@ describe('ssr: renderStyle', () => {
}),
).toBe(`color:"><script;`)
})
+
+ test('useCssVars handling', () => {
+ expect(
+ ssrRenderStyle({
+ fontSize: null,
+ ':--v1': undefined,
+ ':--v2': null,
+ ':--v3': '',
+ ':--v4': ' ',
+ ':--v5': 'foo',
+ ':--v6': 0,
+ '--foo': 1,
+ }),
+ ).toBe(`--v1:initial;--v2:initial;--v3: ;--v4: ;--v5:foo;--v6:0;--foo:1;`)
+ })
})
diff --git a/packages/server-renderer/__tests__/webStream.spec.ts b/packages/server-renderer/__tests__/webStream.spec.ts
index 700a9a0ae..de399dbb8 100644
--- a/packages/server-renderer/__tests__/webStream.spec.ts
+++ b/packages/server-renderer/__tests__/webStream.spec.ts
@@ -49,7 +49,7 @@ test('pipeToWebWritable', async () => {
}
const { readable, writable } = new TransformStream()
- pipeToWebWritable(createApp(App), {}, writable)
+ pipeToWebWritable(createApp(App), {}, writable as any)
const reader = readable.getReader()
const decoder = new TextDecoder()
diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json
index 5be30879d..18ede1625 100644
--- a/packages/server-renderer/package.json
+++ b/packages/server-renderer/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/server-renderer",
- "version": "3.5.14",
+ "version": "3.5.17",
"description": "@vue/server-renderer",
"main": "index.js",
"module": "dist/server-renderer.esm-bundler.js",
diff --git a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts
index 9689b4185..b082da03f 100644
--- a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts
@@ -1,5 +1,7 @@
import {
escapeHtml,
+ isArray,
+ isObject,
isRenderableAttrValue,
isSVGTag,
stringifyStyle,
@@ -12,6 +14,7 @@ import {
isString,
makeMap,
normalizeClass,
+ normalizeCssVarValue,
normalizeStyle,
propsToAttrMap,
} from '@vue/shared'
@@ -93,6 +96,22 @@ export function ssrRenderStyle(raw: unknown): string {
if (isString(raw)) {
return escapeHtml(raw)
}
- const styles = normalizeStyle(raw)
+ const styles = normalizeStyle(ssrResetCssVars(raw))
return escapeHtml(stringifyStyle(styles))
}
+
+function ssrResetCssVars(raw: unknown) {
+ if (!isArray(raw) && isObject(raw)) {
+ const res: Record = {}
+ for (const key in raw) {
+ // `:` prefixed keys are coming from `ssrCssVars`
+ if (key.startsWith(':--')) {
+ res[key.slice(1)] = normalizeCssVarValue(raw[key])
+ } else {
+ res[key] = raw[key]
+ }
+ }
+ return res
+ }
+ return raw
+}
diff --git a/packages/shared/__tests__/cssVars.spec.ts b/packages/shared/__tests__/cssVars.spec.ts
new file mode 100644
index 000000000..747ab067d
--- /dev/null
+++ b/packages/shared/__tests__/cssVars.spec.ts
@@ -0,0 +1,27 @@
+import { normalizeCssVarValue } from '../src'
+
+describe('utils/cssVars', () => {
+ test('should normalize css binding values correctly', () => {
+ expect(normalizeCssVarValue(null)).toBe('initial')
+ expect(normalizeCssVarValue(undefined)).toBe('initial')
+ expect(normalizeCssVarValue('')).toBe(' ')
+ expect(normalizeCssVarValue(' ')).toBe(' ')
+ expect(normalizeCssVarValue('foo')).toBe('foo')
+ expect(normalizeCssVarValue(0)).toBe('0')
+ })
+
+ test('should warn on invalid css binding values', () => {
+ const warning =
+ '[Vue warn] Invalid value used for CSS binding. Expected a string or a finite number but received:'
+ expect(normalizeCssVarValue(NaN)).toBe('NaN')
+ expect(warning).toHaveBeenWarnedTimes(1)
+ expect(normalizeCssVarValue(Infinity)).toBe('Infinity')
+ expect(warning).toHaveBeenWarnedTimes(2)
+ expect(normalizeCssVarValue(-Infinity)).toBe('-Infinity')
+ expect(warning).toHaveBeenWarnedTimes(3)
+ expect(normalizeCssVarValue({})).toBe('[object Object]')
+ expect(warning).toHaveBeenWarnedTimes(4)
+ expect(normalizeCssVarValue([])).toBe('')
+ expect(warning).toHaveBeenWarnedTimes(5)
+ })
+})
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 49935f446..648179768 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/shared",
- "version": "3.5.14",
+ "version": "3.5.17",
"description": "internal utils shared across @vue packages",
"main": "index.js",
"module": "dist/shared.esm-bundler.js",
diff --git a/packages/shared/src/cssVars.ts b/packages/shared/src/cssVars.ts
new file mode 100644
index 000000000..0c69b606f
--- /dev/null
+++ b/packages/shared/src/cssVars.ts
@@ -0,0 +1,24 @@
+/**
+ * Normalize CSS var value created by `v-bind` in `