Files
webawesome/docs/assets/scripts/hydration-errors.js
2024-12-14 17:10:26 -05:00

129 lines
4.1 KiB
JavaScript

/** 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
</button>
<wa-dialog class="diff-dialog" with-header light-dismiss>
<div class="diff-grid">
<div>
<div>Server</div>
<pre class="diff-server"><code></code></pre>
</div>
<div>
<div>Client</div>
<pre class="diff-client"><code></code></pre>
</div>
<div>
<div>Diff</div>
<pre class="diff-viewer"><code></code></pre>
</div>
</div>
</wa-dialog>
`;
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,
clientHTML,
}),
);
});
}
function createDiff({ serverHTML, clientHTML }) {
const diff = diffLines(serverHTML, clientHTML, {
ignoreWhitespace: false,
newLineIsToken: true,
});
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);
})();