Initial SSR implementation (#157)

* continued ssr work

* continued ssr work

* prettier

* all components now rendering

* everything finally works

* fix type issues

* working on breadcrumb

* working on breadcrumb

* radio group

* convert all tests to ssr

* prettier

* test suite finally passing

* add layout stuff

* add changelog

* fix TS issue

* fix tests

* fixing deploy stuff

* get QR code displaying

* fix tests

* fix tests

* prettier

* condense hydration stuff

* prettier

* comment out range test

* fixing issues

* use base fixtures

* fixing examples

* dont vendor

* fix import of hydration support

* adding notes

* add notesg

* add ssr loader

* fix build

* prettier

* add notes

* add notes

* prettier

* fixing bundled stuff

* remove cdn

* remove cdn

* prettier

* fiixng tests

* prettier

* split jobs??

* prettier

* fix build stuff

* add reset mouse and await aTimeout

* prettier

* fix improper tests

* prettier

* bail on first

* fix linting

* only test form with client

* redundancy on ssr-loader??

* maybe this will work

* prettier

* try callout now

* fix form.test.ts

* fix form.test.ts

* prettier

* fix forms

* fix forms

* try again

* prettier

* add some awaits

* prettier

* comment out broken SSR tests

* prettier

* comment out broken SSR tests

* prettier

* dont skip in CI

* upgrade playwright to beta

* prettier

* try some trickery

* try some trickery

* await updateComplete

* try to fix form.test.ts

* import hydrateable elements 1 time

* prettier

* fix input defaultValue issues

* fix form controls to behave like their native counterpartS

* add changelog entry

* prettier

* fix unexpected behavior with range / button
This commit is contained in:
Konnor Rogers
2024-09-11 10:25:42 -04:00
committed by GitHub
parent cd9fa25a2e
commit 14914abf65
129 changed files with 12195 additions and 10038 deletions

View File

@@ -0,0 +1,128 @@
/** 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);
})();

View File

@@ -43,6 +43,30 @@ function restoreScrollPosition(event) {
});
}
function fixDSD(e) {
const newElement = e.detail.newBody || e.detail.newFrame || e.detail.newStream;
if (!newElement) {
return;
}
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#polyfill
(function attachShadowRoots(root) {
root.querySelectorAll('template[shadowrootmode]').forEach(template => {
const mode = template.getAttribute('shadowrootmode');
const shadowRoot = template.parentNode.attachShadow({ mode });
shadowRoot.appendChild(template.content);
template.remove();
attachShadowRoots(shadowRoot);
});
})(newElement);
}
// Fixes an issue with DSD keeping the `<template>` elements hanging around in the lightdom.
// https://github.com/hotwired/turbo/issues/1292
['turbo:before-render', 'turbo:before-stream-render', 'turbo:before-frame-render'].forEach(eventName => {
document.addEventListener(eventName, fixDSD);
});
window.addEventListener('turbo:before-cache', saveScrollPosition);
window.addEventListener('turbo:before-render', restoreScrollPosition);
window.addEventListener('turbo:render', restoreScrollPosition);