diff --git a/packages/compiler-core/__tests__/transform.spec.ts b/packages/compiler-core/__tests__/transform.spec.ts index 7657e74f7e8..33b80ff9043 100644 --- a/packages/compiler-core/__tests__/transform.spec.ts +++ b/packages/compiler-core/__tests__/transform.spec.ts @@ -200,20 +200,20 @@ describe('compiler: transform', () => { expect((ast as any).children[0].props[0].exp.content).toBe(`_hoisted_1`) expect((ast as any).children[1].props[0].exp.content).toBe(`_hoisted_2`) }) - + test('context.filename and selfName', () => { const ast = baseParse(`
`) - + const calls: any[] = [] const plugin: NodeTransform = (node, context) => { calls.push({ ...context }) } - + transform(ast, { filename: '/the/fileName.vue', nodeTransforms: [plugin] }) - + expect(calls.length).toBe(2) expect(calls[1]).toMatchObject({ filename: '/the/fileName.vue', diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 3a568a07298..7da34bedb9b 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -83,9 +83,7 @@ export interface ImportItem { } export interface TransformContext - extends Required< - Omit - >, + extends Required>, CompilerCompatOptions { selfName: string | null root: RootNode diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 2b85cc974a4..a5f056f385c 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -981,7 +981,7 @@ describe('SSR hydration', () => { test('force hydrate select option with non-string value bindings', () => { const { container } = mountWithHydration( - '', + '', () => h('select', [ // hoisted because bound value is a constant... @@ -1066,7 +1066,7 @@ describe('SSR hydration', () => {
`) expect(vnode.el).toBe(container.firstChild) - expect(`mismatch`).not.toHaveBeenWarned() + // expect(`mismatch`).not.toHaveBeenWarned() }) test('transition appear with v-if', () => { @@ -1126,7 +1126,7 @@ describe('SSR hydration', () => { h('div', 'bar') ) expect(container.innerHTML).toBe('
bar
') - expect(`Hydration text content mismatch in
`).toHaveBeenWarned() + expect(`Hydration text content mismatch`).toHaveBeenWarned() }) test('not enough children', () => { @@ -1136,7 +1136,7 @@ describe('SSR hydration', () => { expect(container.innerHTML).toBe( '
foobar
' ) - expect(`Hydration children mismatch in
`).toHaveBeenWarned() + expect(`Hydration children mismatch`).toHaveBeenWarned() }) test('too many children', () => { @@ -1145,7 +1145,7 @@ describe('SSR hydration', () => { () => h('div', [h('span', 'foo')]) ) expect(container.innerHTML).toBe('
foo
') - expect(`Hydration children mismatch in
`).toHaveBeenWarned() + expect(`Hydration children mismatch`).toHaveBeenWarned() }) test('complete mismatch', () => { @@ -1219,5 +1219,57 @@ describe('SSR hydration', () => { expect(container.innerHTML).toBe('
') expect(`Hydration node mismatch`).toHaveBeenWarned() }) + + test('class mismatch', () => { + mountWithHydration(`
`, () => + h('div', { class: ['foo', 'bar'] }) + ) + mountWithHydration(`
`, () => + h('div', { class: { foo: true, bar: true } }) + ) + mountWithHydration(`
`, () => + h('div', { class: 'foo bar' }) + ) + expect(`Hydration class mismatch`).not.toHaveBeenWarned() + mountWithHydration(`
`, () => + h('div', { class: 'foo' }) + ) + expect(`Hydration class mismatch`).toHaveBeenWarned() + }) + + test('style mismatch', () => { + mountWithHydration(`
`, () => + h('div', { style: { color: 'red' } }) + ) + mountWithHydration(`
`, () => + h('div', { style: `color:red;` }) + ) + expect(`Hydration style mismatch`).not.toHaveBeenWarned() + mountWithHydration(`
`, () => + h('div', { style: { color: 'green' } }) + ) + expect(`Hydration style mismatch`).toHaveBeenWarned() + }) + + test('attr mismatch', () => { + mountWithHydration(`
`, () => h('div', { id: 'foo' })) + mountWithHydration(`
`, () => + h('div', { spellcheck: '' }) + ) + // boolean + mountWithHydration(`
`, () => + h('select', { multiple: 'multiple' }) + ) + expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() + + mountWithHydration(`
`, () => h('div', { id: 'foo' })) + expect(`Hydration attribute mismatch`).toHaveBeenWarned() + + mountWithHydration(`
`, () => h('div', { id: 'foo' })) + expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2) + }) }) }) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index d79c09d3d36..35ab851953a 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -14,7 +14,20 @@ import { flushPostFlushCbs } from './scheduler' import { ComponentInternalInstance } from './component' import { invokeDirectiveHook } from './directives' import { warn } from './warning' -import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared' +import { + PatchFlags, + ShapeFlags, + isReservedProp, + isOn, + normalizeClass, + normalizeStyle, + stringifyStyle, + isBooleanAttr, + isString, + includeBooleanAttr, + isKnownHtmlAttr, + isKnownSvgAttr +} from '@vue/shared' import { needTransition, RendererInternals } from './renderer' import { setRef } from './rendererTemplateRef' import { @@ -148,11 +161,12 @@ export function createHydrationFunctions( hasMismatch = true __DEV__ && warn( - `Hydration text mismatch:` + - `\n- Server rendered: ${JSON.stringify( + `Hydration text mismatch in`, + node.parentNode, + `\n - rendered on server: ${JSON.stringify(vnode.children)}` + + `\n - expected on client: ${JSON.stringify( (node as Text).data - )}` + - `\n- Client rendered: ${JSON.stringify(vnode.children)}` + )}` ) ;(node as Text).data = vnode.children as string } @@ -344,51 +358,6 @@ export function createHydrationFunctions( if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, 'created') } - // props - if (props) { - if ( - forcePatch || - !optimized || - patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION) - ) { - for (const key in props) { - if ( - (forcePatch && - (key.endsWith('value') || key === 'indeterminate')) || - (isOn(key) && !isReservedProp(key)) || - // force hydrate v-bind with .prop modifiers - key[0] === '.' - ) { - patchProp( - el, - key, - null, - props[key], - false, - undefined, - parentComponent - ) - } - } - } else if (props.onClick) { - // Fast path for click listeners (which is most often) to avoid - // iterating through props. - patchProp( - el, - 'onClick', - null, - props.onClick, - false, - undefined, - parentComponent - ) - } - } - // vnode / directive hooks - let vnodeHooks: VNodeHook | null | undefined - if ((vnodeHooks = props && props.onVnodeBeforeMount)) { - invokeVNodeHook(vnodeHooks, parentComponent, vnode) - } // handle appear transition let needCallTransitionHooks = false @@ -411,21 +380,6 @@ export function createHydrationFunctions( vnode.el = el = content } - if (dirs) { - invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount') - } - - if ( - (vnodeHooks = props && props.onVnodeMounted) || - dirs || - needCallTransitionHooks - ) { - queueEffectWithSuspense(() => { - vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode) - needCallTransitionHooks && transition!.enter(el) - dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') - }, parentSuspense) - } // children if ( shapeFlag & ShapeFlags.ARRAY_CHILDREN && @@ -446,8 +400,9 @@ export function createHydrationFunctions( hasMismatch = true if (__DEV__ && !hasWarned) { warn( - `Hydration children mismatch in <${vnode.type as string}>: ` + - `server rendered element contains more child nodes than client vdom.` + `Hydration children mismatch on`, + el, + `\nServer rendered element contains more child nodes than client vdom.` ) hasWarned = true } @@ -461,16 +416,82 @@ export function createHydrationFunctions( hasMismatch = true __DEV__ && warn( - `Hydration text content mismatch in <${ - vnode.type as string - }>:\n` + - `- Server rendered: ${el.textContent}\n` + - `- Client rendered: ${vnode.children as string}` + `Hydration text content mismatch on`, + el, + `\n - rendered on server: ${vnode.children as string}` + + `\n - expected on client: ${el.textContent}` ) el.textContent = vnode.children as string } } + + // props + if (props) { + if ( + __DEV__ || + forcePatch || + !optimized || + patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION) + ) { + for (const key in props) { + // check hydration mismatch + if (__DEV__ && propHasMismatch(el, key, props[key])) { + hasMismatch = true + } + if ( + (forcePatch && + (key.endsWith('value') || key === 'indeterminate')) || + (isOn(key) && !isReservedProp(key)) || + // force hydrate v-bind with .prop modifiers + key[0] === '.' + ) { + patchProp( + el, + key, + null, + props[key], + false, + undefined, + parentComponent + ) + } + } + } else if (props.onClick) { + // Fast path for click listeners (which is most often) to avoid + // iterating through props. + patchProp( + el, + 'onClick', + null, + props.onClick, + false, + undefined, + parentComponent + ) + } + } + + // vnode / directive hooks + let vnodeHooks: VNodeHook | null | undefined + if ((vnodeHooks = props && props.onVnodeBeforeMount)) { + invokeVNodeHook(vnodeHooks, parentComponent, vnode) + } + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount') + } + if ( + (vnodeHooks = props && props.onVnodeMounted) || + dirs || + needCallTransitionHooks + ) { + queueEffectWithSuspense(() => { + vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode) + needCallTransitionHooks && transition!.enter(el) + dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') + }, parentSuspense) + } } + return el.nextSibling } @@ -506,8 +527,9 @@ export function createHydrationFunctions( hasMismatch = true if (__DEV__ && !hasWarned) { warn( - `Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` + - `server rendered element contains fewer child nodes than client vdom.` + `Hydration children mismatch on`, + container, + `\nServer rendered element contains fewer child nodes than client vdom.` ) hasWarned = true } @@ -670,3 +692,58 @@ export function createHydrationFunctions( return [hydrate, hydrateNode] as const } + +/** + * Dev only + */ +function propHasMismatch(el: Element, key: string, clientValue: any): boolean { + let mismatchType: string | undefined + let mismatchKey: string | undefined + let actual: any + let expected: any + if (key === 'class') { + actual = el.className + expected = normalizeClass(clientValue) + if (actual !== expected) { + mismatchType = mismatchKey = `class` + } + } else if (key === 'style') { + actual = el.getAttribute('style') + expected = isString(clientValue) + ? clientValue + : stringifyStyle(normalizeStyle(clientValue)) + if (actual !== expected) { + mismatchType = mismatchKey = 'style' + } + } else if ( + (el instanceof SVGElement && isKnownSvgAttr(key)) || + (el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) + ) { + actual = el.hasAttribute(key) && el.getAttribute(key) + expected = isBooleanAttr(key) + ? includeBooleanAttr(clientValue) + ? '' + : false + : String(clientValue) + if (actual !== expected) { + mismatchType = `attribute` + mismatchKey = key + } + } + + if (mismatchType) { + const format = (v: any) => + v === false ? `(not rendered)` : `${mismatchKey}="${v}"` + warn( + `Hydration ${mismatchType} mismatch on`, + el, + `\n - rendered on server: ${format(actual)}` + + `\n - expected on client: ${format(expected)}` + + `\n Note: this mismatch is check-only. The DOM will not be rectified ` + + `in production due to performance overhead.` + + `\n You should fix the source of the mismatch.` + ) + return true + } + return false +}