From 14914abf65ce5a308950f80779aa3d0e6ea23d86 Mon Sep 17 00:00:00 2001 From: Konnor Rogers Date: Wed, 11 Sep 2024 10:25:42 -0400 Subject: [PATCH] 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 --- .github/workflows/node.js.yml | 87 +- .gitignore | 5 +- custom-elements-manifest.js | 4 +- docs/.eleventy.js | 22 + docs/_includes/base.njk | 6 +- docs/assets/scripts/hydration-errors.js | 128 ++ docs/assets/scripts/turbo.js | 24 + docs/assets/styles/docs.css | 58 +- docs/assets/styles/hydration-errors.css | 38 + docs/docs/components/animation.md | 2 +- docs/docs/components/checkbox.md | 2 +- docs/docs/components/dialog.md | 2 +- docs/docs/components/input.md | 2 +- docs/docs/components/radio-group.md | 2 +- docs/docs/components/rating.md | 15 +- docs/docs/components/tooltip.md | 2 +- docs/docs/experimental/ssr.md | 111 ++ docs/docs/resources/changelog.md | 6 + docs/docs/resources/contributing.md | 16 +- docs/index.md | 11 +- index.html | 0 package-lock.json | 417 ++++- package.json | 9 +- scripts/build.js | 77 +- scripts/utils.js | 1 + .../animated-image/animated-image.test.ts | 95 +- src/components/animation/animation.test.ts | 163 +- src/components/avatar/avatar.test.ts | 323 ++-- src/components/badge/badge.test.ts | 124 +- .../breadcrumb-item/breadcrumb-item.test.ts | 351 ++-- src/components/breadcrumb/breadcrumb.test.ts | 219 +-- .../button-group/button-group.test.ts | 183 +- src/components/button/button.test.ts | 584 +++---- src/components/button/button.ts | 4 +- src/components/callout/callout.test.ts | 34 +- src/components/card/card.test.ts | 348 ++-- .../carousel-item/carousel-item.test.ts | 28 +- src/components/carousel/carousel.test.ts | 1457 ++++++++-------- src/components/carousel/carousel.ts | 34 +- src/components/checkbox/checkbox.test.ts | 745 ++++---- src/components/checkbox/checkbox.ts | 59 +- .../color-picker/color-picker.test.ts | 1017 +++++------ src/components/color-picker/color-picker.ts | 90 +- .../copy-button/copy-button.test.ts | 23 +- src/components/details/details.test.ts | 382 ++--- src/components/details/details.ts | 2 +- src/components/dialog/dialog.test.ts | 219 +-- src/components/dialog/dialog.ts | 10 +- src/components/divider/divider.test.ts | 56 +- src/components/drawer/drawer.test.ts | 217 +-- src/components/drawer/drawer.ts | 16 +- src/components/dropdown/dropdown.test.ts | 660 ++++---- .../format-bytes/format-bytes.test.ts | 218 +-- .../format-date/format-date.test.ts | 505 +++--- .../format-number/format-number.test.ts | 325 ++-- .../icon-button/icon-button.test.ts | 320 ++-- src/components/icon/icon.test.ts | 378 +++-- src/components/icon/icon.ts | 21 +- .../image-comparer/image-comparer.test.ts | 402 ++--- .../image-comparer/image-comparer.ts | 2 +- src/components/include/include.test.ts | 73 +- src/components/input/input.test.ts | 1064 ++++++------ src/components/input/input.ts | 50 +- src/components/menu-item/menu-item.test.ts | 338 ++-- src/components/menu-item/menu-item.ts | 20 +- .../menu-item/submenu-controller.ts | 8 +- src/components/menu-label/menu-label.test.ts | 16 +- src/components/menu/menu.test.ts | 229 +-- .../mutation-observer.test.ts | 16 +- .../mutation-observer/mutation-observer.ts | 8 +- src/components/option/option.test.ts | 92 +- src/components/page/page.styles.ts | 20 +- src/components/page/page.test.ts | 16 +- src/components/page/page.ts | 78 +- src/components/popup/popup.test.ts | 16 +- .../progress-bar/progress-bar.test.ts | 162 +- .../progress-ring/progress-ring.test.ts | 100 +- src/components/qr-code/qr-code.styles.ts | 9 + src/components/qr-code/qr-code.test.ts | 74 +- src/components/qr-code/qr-code.ts | 42 +- .../radio-button/radio-button.test.ts | 79 +- src/components/radio-button/radio-button.ts | 25 +- .../radio-group/radio-group.test.ts | 766 ++++----- src/components/radio-group/radio-group.ts | 75 +- src/components/radio/radio.test.ts | 36 +- src/components/radio/radio.ts | 10 +- src/components/range/range.test.ts | 440 ++--- src/components/range/range.ts | 40 +- src/components/rating/rating.test.ts | 196 +-- src/components/rating/rating.ts | 4 +- .../relative-time/relative-time.test.ts | 221 +-- src/components/relative-time/relative-time.ts | 4 +- .../resize-observer/resize-observer.test.ts | 19 + .../resize-observer/resize-observer.ts | 4 +- src/components/select/select.test.ts | 1109 ++++++------ src/components/select/select.ts | 91 +- src/components/skeleton/skeleton.test.ts | 44 +- src/components/spinner/spinner.test.ts | 38 +- .../split-panel/split-panel.test.ts | 498 +++--- src/components/split-panel/split-panel.ts | 10 +- src/components/switch/switch.test.ts | 630 +++---- src/components/switch/switch.ts | 49 +- src/components/tab-group/tab-group.test.ts | 770 ++++----- src/components/tab-group/tab-group.ts | 2 +- src/components/tab-panel/tab-panel.test.ts | 68 +- src/components/tab/tab.test.ts | 78 +- src/components/tab/tab.ts | 2 +- src/components/tag/tag.test.ts | 84 +- src/components/textarea/textarea.test.ts | 592 +++---- src/components/textarea/textarea.ts | 45 +- src/components/tooltip/tooltip.test.ts | 250 +-- src/components/tooltip/tooltip.ts | 8 + src/components/tree-item/tree-item.test.ts | 326 ++-- src/components/tree-item/tree-item.ts | 2 +- src/components/tree/tree.test.ts | 1495 +++++++++-------- src/components/tree/tree.ts | 10 +- .../visually-hidden/visually-hidden.test.ts | 62 +- src/internal/form.test.ts | 154 +- src/internal/scroll.ts | 21 +- src/internal/scrollend-polyfill.ts | 125 +- src/internal/test/fixture.ts | 102 ++ src/internal/test/form-control-base-tests.ts | 387 +++-- src/internal/validators/required-validator.ts | 20 +- src/internal/webawesome-element.test.ts | 4 +- src/internal/webawesome-element.ts | 72 +- src/themes/applied.css | 11 +- src/utilities/base-path.ts | 5 +- src/webawesome.ssr-loader.ts | 3 + web-test-runner.config.js | 55 +- 129 files changed, 12195 insertions(+), 10038 deletions(-) create mode 100644 docs/assets/scripts/hydration-errors.js create mode 100644 docs/assets/styles/hydration-errors.css create mode 100644 docs/docs/experimental/ssr.md create mode 100644 index.html create mode 100644 src/components/resize-observer/resize-observer.test.ts create mode 100644 src/internal/test/fixture.ts create mode 100644 src/webawesome.ssr-loader.ts diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index ff8b44e35..dbc3a46d1 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -1,5 +1,36 @@ -# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions +# # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +# name: Node.js CI + +# on: +# push: +# branches: [next] +# pull_request: +# branches: [next] + +# jobs: +# build: +# runs-on: ubuntu-latest + +# strategy: +# matrix: +# node-version: [20.x] +# # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + +# steps: +# - uses: actions/checkout@v4 +# - name: Use Node.js ${{ matrix.node-version }} +# uses: actions/setup-node@v4 +# with: +# node-version: ${{ matrix.node-version }} +# cache: 'npm' +# - run: npm ci +# - run: npx playwright install-deps +# - run: npm run verify + +# # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI @@ -10,21 +41,61 @@ on: branches: [next] jobs: - build: + lint: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x] + node-version: [20.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npx playwright install-deps - run: npm ci - - run: npm run verify + - run: npm run prettier && npm run lint + + test_client: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npx playwright uninstall --all && npx playwright install --force chromium firefox webkit --with-deps + - run: npm run build + # --bail to fail on first failing test. + - run: CSR_ONLY="true" npm run test -- --bail + + test_ssr: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npx playwright uninstall --all && npx playwright install --force chromium firefox webkit --with-deps + - run: npm run build + - run: SSR_ONLY="true" npm run test -- --bail diff --git a/.gitignore b/.gitignore index aa943f40d..b5bb02845 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,10 @@ _site .DS_Store package.json package-lock.json -dist +dist/ +dist-cdn/ docs/public/pagefind node_modules src/react -cdn .astro +cdn/ diff --git a/custom-elements-manifest.js b/custom-elements-manifest.js index cdf80b9d6..2c1853295 100644 --- a/custom-elements-manifest.js +++ b/custom-elements-manifest.js @@ -7,7 +7,7 @@ import fs from 'fs'; const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8')); const { name, description, version, author, homepage, license } = packageData; -const outdir = 'dist'; +const outdir = 'dist-cdn'; function replace(string, terms) { terms.forEach(({ from, to }) => { @@ -162,7 +162,7 @@ export default { }), customElementJetBrainsPlugin({ - outdir: './dist', + outdir: './dist-cdn', excludeCss: true, packageJson: false, referencesTemplate: (_, tag) => { diff --git a/docs/.eleventy.js b/docs/.eleventy.js index fe807b788..1630652d7 100644 --- a/docs/.eleventy.js +++ b/docs/.eleventy.js @@ -11,6 +11,8 @@ import { searchPlugin } from './_utils/search.js'; import { readFile } from 'fs/promises'; import { outlinePlugin } from './_utils/outline.js'; import { getComponents } from './_utils/manifest.js'; +import litPlugin from '@lit-labs/eleventy-plugin-lit'; + import process from 'process'; const packageData = JSON.parse(await readFile('./package.json', 'utf-8')); @@ -106,6 +108,26 @@ export default function (eleventyConfig) { ]) ); + const omittedModules = []; + + // problematic components: + // animation (breaks on navigation + ssr with Turbo) + // mutation-observer (why SSR this?) + // resize-observer (why SSR this?) + // tooltip (why SSR this?) + + const componentModules = getComponents() + // .filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1])) + .map(component => { + const name = component.tagName.split(/wa-/)[1]; + return `./dist/components/${name}/${name}.js`; + }); + + eleventyConfig.addPlugin(litPlugin, { + mode: 'worker', + componentModules + }); + // Build the search index eleventyConfig.addPlugin( searchPlugin({ diff --git a/docs/_includes/base.njk b/docs/_includes/base.njk index 95c2722ae..6ecbedfb2 100644 --- a/docs/_includes/base.njk +++ b/docs/_includes/base.njk @@ -13,6 +13,9 @@ {# Scripts #} + {# Hydration stuff #} + + @@ -26,7 +29,7 @@ {# Web Awesome #} - + @@ -128,5 +131,6 @@ {% include 'search.njk' %} + diff --git a/docs/assets/scripts/hydration-errors.js b/docs/assets/scripts/hydration-errors.js new file mode 100644 index 000000000..c6def88a8 --- /dev/null +++ b/docs/assets/scripts/hydration-errors.js @@ -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 = ``; + 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 = ` + + +
+
+
Server
+
+
+
+
Client
+
+
+
+
Diff
+
+
+
+
+ `; + + 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); +})(); diff --git a/docs/assets/scripts/turbo.js b/docs/assets/scripts/turbo.js index 9de36c966..ad76069be 100644 --- a/docs/assets/scripts/turbo.js +++ b/docs/assets/scripts/turbo.js @@ -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 `