From e210d8f122b3e3f2b9f0850f347c8513010316aa Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Tue, 31 Dec 2024 13:18:49 -0700 Subject: [PATCH] fix: disable_static_content_optimization test exceptions, logging refactor --- .../engine-core/src/framework/hydration.ts | 129 +++++++++++------- .../favors-client-side-text/index.spec.js | 22 ++- .../if-true/index.spec.js | 25 +++- 3 files changed, 114 insertions(+), 62 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/hydration.ts b/packages/@lwc/engine-core/src/framework/hydration.ts index 27446a088e..90b49e117c 100644 --- a/packages/@lwc/engine-core/src/framework/hydration.ts +++ b/packages/@lwc/engine-core/src/framework/hydration.ts @@ -82,54 +82,86 @@ let hasMismatch = false; // Errors queued during hydration process. Flushed after the node has been mounted. let hydrationErrors: Array = []; +enum HydrationErrorTypes { + Node = 'node', + Attribute = 'attribute', + InnerHTML = 'innerHTML', + Comment = 'comment', + TextContent = 'text content', + ChildNode = 'child node', +} + class HydrationError { - errorType: string; + type: HydrationErrorTypes; serverRendered: any; clientExpected: any; - constructor(errorType: string, serverRendered?: any, clientExpected?: any) { - this.errorType = errorType; + private constructor(type: HydrationErrorTypes, serverRendered?: any, clientExpected?: any) { + this.type = type; this.serverRendered = serverRendered; this.clientExpected = clientExpected; } - log(context?: Node | null) { + public static create( + type: HydrationErrorTypes, + serverRendered?: any, + clientExpected?: any + ): HydrationError { + return new HydrationError(type, serverRendered, clientExpected); + } + + public static node(serverRendered?: any): HydrationError { + return new HydrationError(HydrationErrorTypes.Node, serverRendered); + } + + public static attribute( + attributeName: string, + serverRendered?: any, + clientExpected?: any + ): HydrationError { + return new HydrationError( + HydrationErrorTypes.Attribute, + `${attributeName}=${serverRendered}`, + `${attributeName}=${clientExpected}` + ); + } + + log(source?: Node | null) { if (process.env.NODE_ENV !== 'production') { logWarn( - `Hydration ${this.errorType} mismatch on:`, - context, + `Hydration ${this.type} mismatch on:`, + source, `\n- rendered on server:`, this.serverRendered, `\n- expected on client:`, - this.clientExpected + this.clientExpected || source ); } } } -class NodeHydrationError extends HydrationError { - constructor(serverRendered?: any) { - super('node', serverRendered); - } - - log(context?: Node | null) { - this.clientExpected = context; - super.log(context); +function isTypeElement(node?: Node): node is Element { + const isCorrectType = node?.nodeType === EnvNodeTypes.ELEMENT; + if (!isCorrectType) { + queueHydrationError(HydrationError.node(node)); } + return isCorrectType; } -class AttributeHydrationError extends HydrationError { - constructor(attribute: string, serverRendered?: any, clientExpected?: any) { - super('attribute', `${attribute}=${serverRendered}`, `${attribute}=${clientExpected}`); +function isTypeText(node?: Node): node is Text { + const isCorrectType = node?.nodeType === EnvNodeTypes.TEXT; + if (!isCorrectType) { + queueHydrationError(HydrationError.node(node)); } + return isCorrectType; } -function isTypeElement(node?: Node): node is Element { - if (node?.nodeType !== EnvNodeTypes.ELEMENT) { - queueHydrationError(new NodeHydrationError(node)); - return false; +function isTypeComment(node?: Node): node is Comment { + const isCorrectType = node?.nodeType === EnvNodeTypes.COMMENT; + if (!isCorrectType) { + queueHydrationError(HydrationError.node(node)); } - return true; + return isCorrectType; } /* @@ -141,6 +173,10 @@ function logWarn(...args: any) { console.warn('[LWC warn:', ...args); } +function prettyPrint(set: Classes) { + return JSON.stringify(ArrayJoin.call(ArraySort.call(ArrayFrom(set)), ' ')); +} + export function hydrateRoot(vm: VM) { hasMismatch = false; @@ -238,7 +274,9 @@ function validateEqualTextNodeContent( return true; } - queueHydrationError(new HydrationError('text content', nodeValue, vnode.text)); + queueHydrationError( + HydrationError.create(HydrationErrorTypes.TextContent, nodeValue, vnode.text) + ); return false; } @@ -296,8 +334,7 @@ function getValidationPredicate( } function hydrateText(node: Node, vnode: VText, renderer: RendererAPI): Node | null { - if (node?.nodeType !== EnvNodeTypes.TEXT) { - hydrationErrors.push(new NodeHydrationError(node)); + if (!isTypeText(node)) { return handleMismatch(node, vnode, renderer); } return updateTextContent(node, vnode, renderer); @@ -319,9 +356,7 @@ function updateTextContent( } function hydrateComment(node: Node, vnode: VComment, renderer: RendererAPI): Node | null { - //if (!hasCorrectNodeType(vnode, node, EnvNodeTypes.COMMENT, renderer)) { - if (node?.nodeType !== EnvNodeTypes.COMMENT) { - hydrationErrors.push(new NodeHydrationError(node)); + if (!isTypeComment(node)) { return handleMismatch(node, vnode, renderer); } if (process.env.NODE_ENV !== 'production') { @@ -329,14 +364,16 @@ function hydrateComment(node: Node, vnode: VComment, renderer: RendererAPI): Nod const nodeValue = getProperty(node, NODE_VALUE_PROP); if (nodeValue !== vnode.text) { - queueHydrationError(new HydrationError('comment', nodeValue, vnode.text)); + queueHydrationError( + HydrationError.create(HydrationErrorTypes.Comment, nodeValue, vnode.text) + ); } } const { setProperty } = renderer; // We only set the `nodeValue` property here (on a comment), so we don't need // to sanitize the content as HTML using `safelySetProperty` - setProperty(node as Element, NODE_VALUE_PROP, vnode.text ?? null); + setProperty(node, NODE_VALUE_PROP, vnode.text ?? null); vnode.elm = node; return node; @@ -413,8 +450,8 @@ function hydrateElement(elm: Node, vnode: VElement, renderer: RendererAPI): Node }; } else { queueHydrationError( - new HydrationError( - 'innerHTML', + HydrationError.create( + HydrationErrorTypes.InnerHTML, unwrappedServerInnerHTML, unwrappedClientInnerHTML ) @@ -555,7 +592,9 @@ function hydrateChildren( // We can't know exactly which node(s) caused the delta, but we can provide context (parent) and the mismatched sets if (process.env.NODE_ENV !== 'production') { const clientNodes = children.map((c) => c?.elm); - queueHydrationError(new HydrationError('child node', serverNodes, clientNodes)); + queueHydrationError( + HydrationError.create(HydrationErrorTypes.ChildNode, serverNodes, clientNodes) + ); } } } @@ -584,7 +623,7 @@ function isMatchingElement( ) { const { getProperty } = renderer; if (vnode.sel.toLowerCase() !== getProperty(elm, 'tagName').toLowerCase()) { - queueHydrationError(new NodeHydrationError(elm)); + queueHydrationError(HydrationError.node(elm)); return false; } @@ -641,7 +680,7 @@ function validateAttrs( const elmAttrValue = getAttribute(elm, attrName); if (!attributeValuesAreEqual(attrValue, elmAttrValue)) { queueHydrationError( - new AttributeHydrationError( + HydrationError.attribute( attrName, isNull(elmAttrValue) ? elmAttrValue : `"${elmAttrValue}"`, isNull(attrValue) ? attrValue : `"${attrValue}"` @@ -732,11 +771,9 @@ function validateClassAttr( const classesAreCompatible = checkClassesCompatibility(vnodeClasses, elmClasses); - if (process.env.NODE_ENV !== 'production' && !classesAreCompatible) { - const prettyPrint = (set: Classes) => - JSON.stringify(ArrayJoin.call(ArraySort.call(ArrayFrom(set)), ' ')); + if (!classesAreCompatible) { queueHydrationError( - new AttributeHydrationError('class', prettyPrint(elmClasses), prettyPrint(vnodeClasses)) + HydrationError.attribute('class', prettyPrint(elmClasses), prettyPrint(vnodeClasses)) ); } @@ -786,9 +823,7 @@ function validateStyleAttr( } if (!nodesAreCompatible) { - queueHydrationError( - new AttributeHydrationError('style', `"${elmStyle}"`, `"${vnodeStyle}"`) - ); + queueHydrationError(HydrationError.attribute('style', `"${elmStyle}"`, `"${vnodeStyle}"`)); } return nodesAreCompatible; @@ -805,7 +840,7 @@ function areStaticNodesCompatible( let isCompatibleElements = true; if (getProperty(clientNode, 'tagName') !== getProperty(serverNode, 'tagName')) { - queueHydrationError(new NodeHydrationError(serverNode)); + queueHydrationError(HydrationError.node(serverNode)); return false; } @@ -823,7 +858,7 @@ function areStaticNodesCompatible( // partId === 0 will always refer to the root element, this is guaranteed by the compiler. if (parts?.[0].partId !== 0) { queueHydrationError( - new AttributeHydrationError( + HydrationError.attribute( attrName, `"${serverAttributeValue}"`, `"${clientAttributeValue}"` @@ -851,7 +886,6 @@ function haveCompatibleStaticParts(vnode: VStatic, renderer: RendererAPI) { for (const part of parts) { const { elm } = part; if (isVStaticPartElement(part)) { - // !hasCorrectNodeType(vnode, elm!, EnvNodeTypes.ELEMENT, renderer) if (!isTypeElement(elm)) { return false; } @@ -873,8 +907,7 @@ function haveCompatibleStaticParts(vnode: VStatic, renderer: RendererAPI) { } } else { // VStaticPartText - if (elm?.nodeType !== EnvNodeTypes.TEXT) { - queueHydrationError(new NodeHydrationError(elm)); + if (!isTypeText(elm)) { return false; } updateTextContent(elm, part as VStaticPartText, renderer); diff --git a/packages/@lwc/integration-karma/test-hydration/mismatches/favors-client-side-text/index.spec.js b/packages/@lwc/integration-karma/test-hydration/mismatches/favors-client-side-text/index.spec.js index e2f2269654..ec01449a1b 100644 --- a/packages/@lwc/integration-karma/test-hydration/mismatches/favors-client-side-text/index.spec.js +++ b/packages/@lwc/integration-karma/test-hydration/mismatches/favors-client-side-text/index.spec.js @@ -17,12 +17,20 @@ export default { expect(p).toBe(snapshots.p); expect(p.firstChild).toBe(snapshots.text); expect(p.textContent).toBe('bye!'); - - TestUtils.expectConsoleCallsDev(consoleCalls, { - error: [], - warn: [ - 'Hydration text content mismatch on: P - rendered on server: hello! - expected on client: bye!', - ], - }); + if (process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION) { + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration text content mismatch on: #text - rendered on server: hello! - expected on client: bye!', + ], + }); + } else { + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration text content mismatch on: P - rendered on server: hello! - expected on client: bye!', + ], + }); + } }, }; diff --git a/packages/@lwc/integration-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/index.spec.js b/packages/@lwc/integration-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/index.spec.js index 4d8b727cdc..a5b8aaa358 100644 --- a/packages/@lwc/integration-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/index.spec.js +++ b/packages/@lwc/integration-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/index.spec.js @@ -15,12 +15,23 @@ export default { expect(hydratedSnapshot.text).not.toBe(snapshots.text); - TestUtils.expectConsoleCallsDev(consoleCalls, { - error: [], - warn: [ - 'Hydration child node mismatch on: UL - rendered on server: LI,LI,LI - expected on client: LI,,LI', - 'Hydration completed with errors.', - ], - }); + if (process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION) { + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration text content mismatch on: #text - rendered on server: blue - expected on client: green', + 'Hydration child node mismatch on: UL - rendered on server: LI,LI,LI - expected on client: LI,,LI', + 'Hydration completed with errors.', + ], + }); + } else { + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration child node mismatch on: UL - rendered on server: LI,LI,LI - expected on client: LI,,LI', + 'Hydration completed with errors.', + ], + }); + } }, };