2024-09-11 10:25:42 -04:00
/** TODO: This should probably get abstracted into an actual package. This is listens to the "lit-hydration-error" and then will add a button to show a dialog of the diff. */
( async ( ) => {
const hostname = new URL ( document . baseURI ) . hostname ;
// Only diff on localhost. We dont need to show hydration errors on main site. Only locally.
if ( hostname !== 'localhost' ) {
return ;
}
const { diffLines } = await import ( 'https://cdn.jsdelivr.net/npm/diff@5.2.0/+esm' ) ;
const { getDiffableHTML } = await import (
'https://cdn.jsdelivr.net/npm/@open-wc/semantic-dom-diff@0.20.1/get-diffable-html.js/+esm'
) ;
function wrap ( el , wrapper ) {
el . parentNode . insertBefore ( wrapper , el ) ;
wrapper . appendChild ( el ) ;
}
function handleLitHydrationError ( e ) {
const element = e . target ;
const scratch = document . createElement ( 'div' ) ;
const node = element . cloneNode ( true ) ;
scratch . append ( node ) ;
document . body . append ( scratch ) ;
customElements . upgrade ( node ) ;
node . updateComplete . then ( ( ) => {
// Render styles.
const elementStyles = element . constructor . elementStyles ;
const finalStyles = [ ] ;
if ( elementStyles !== undefined && elementStyles . length > 0 ) {
for ( const style of elementStyles ) {
finalStyles . push ( style . cssText ) ;
}
}
let innerHTML = scratch . firstElementChild ? . shadowRoot . innerHTML ;
if ( finalStyles ? . length ) {
const styleTag = ` <style> ${ finalStyles . join ( '\n' ) } </style> ` ;
innerHTML = styleTag + '\n' + innerHTML ;
}
const clientHTML = getDiffableHTML ( innerHTML ) ;
const serverHTML = getDiffableHTML ( element . shadowRoot ? . innerHTML ) ;
const diffDebugger = document . createElement ( 'div' ) ;
diffDebugger . className = 'diff-debugger' ;
diffDebugger . innerHTML = `
< button class = "diff-dialog-toggle" >
Show Hydration Mismatch
< / b u t t o n >
2025-05-12 10:51:49 -04:00
< wa - dialog class = "diff-dialog" light - dismiss >
2024-09-11 10:25:42 -04:00
< div class = "diff-grid" >
< div >
< div > Server < / d i v >
< pre class = "diff-server" > < code > < / c o d e > < / p r e >
< / d i v >
< div >
< div > Client < / d i v >
< pre class = "diff-client" > < code > < / c o d e > < / p r e >
< / d i v >
< div >
< div > Diff < / d i v >
< pre class = "diff-viewer" > < code > < / c o d e > < / p r e >
< / d i v >
< / d i v >
< / w a - d i a l o g >
` ;
element . focus ( ) ;
wrap ( element , diffDebugger ) ;
diffDebugger . querySelector ( '.diff-server > code' ) . textContent = serverHTML ;
diffDebugger . querySelector ( '.diff-client > code' ) . textContent = clientHTML ;
const diffViewer = diffDebugger . querySelector ( '.diff-viewer > code' ) ;
diffViewer . innerHTML = '' ;
diffViewer . appendChild (
createDiff ( {
serverHTML ,
2024-12-14 17:08:29 -05:00
clientHTML ,
} ) ,
2024-09-11 10:25:42 -04:00
) ;
} ) ;
}
function createDiff ( { serverHTML , clientHTML } ) {
const diff = diffLines ( serverHTML , clientHTML , {
ignoreWhitespace : false ,
2024-12-14 17:08:29 -05:00
newLineIsToken : true ,
2024-09-11 10:25:42 -04:00
} ) ;
const fragment = document . createDocumentFragment ( ) ;
for ( var i = 0 ; i < diff . length ; i ++ ) {
if ( diff [ i ] . added && diff [ i + 1 ] && diff [ i + 1 ] . removed ) {
var swap = diff [ i ] ;
diff [ i ] = diff [ i + 1 ] ;
diff [ i + 1 ] = swap ;
}
var node ;
if ( diff [ i ] . removed ) {
node = document . createElement ( 'del' ) ;
node . appendChild ( document . createTextNode ( diff [ i ] . value ) ) ;
} else if ( diff [ i ] . added ) {
node = document . createElement ( 'ins' ) ;
node . appendChild ( document . createTextNode ( diff [ i ] . value ) ) ;
} else {
node = document . createTextNode ( diff [ i ] . value ) ;
}
fragment . appendChild ( node ) ;
}
return fragment ;
}
function handleDialogToggle ( e ) {
const button = e . composedPath ( ) . find ( el => {
return el . classList && el . classList . contains ( 'diff-dialog-toggle' ) ;
} ) ;
if ( button ) {
button . parentElement . querySelector ( '.diff-dialog' ) . open = true ;
}
}
document . addEventListener ( 'lit-hydration-error' , handleLitHydrationError ) ;
document . addEventListener ( 'click' , handleDialogToggle ) ;
} ) ( ) ;