From 9dedef31ec9a52d4c704e195ed96612e351399c7 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Thu, 9 Jan 2025 13:09:56 -0500 Subject: [PATCH 1/4] preserve HTML entities when prerendering markdown with WCC --- packages/cli/src/lifecycles/prerender.js | 5 +- .../build.config.prerender-markdown.spec.js | 73 +++++++++++++++++++ .../greenwood.config.js | 3 + .../src/components/x-ctc.js | 5 ++ .../src/pages/index.md | 35 +++++++++ .../build.default.markdown.spec.js | 8 +- 6 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 packages/cli/test/cases/build.config.prerender-markdown/build.config.prerender-markdown.spec.js create mode 100644 packages/cli/test/cases/build.config.prerender-markdown/greenwood.config.js create mode 100644 packages/cli/test/cases/build.config.prerender-markdown/src/components/x-ctc.js create mode 100644 packages/cli/test/cases/build.config.prerender-markdown/src/pages/index.md diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js index 785c95d61..2eaff793e 100644 --- a/packages/cli/src/lifecycles/prerender.js +++ b/packages/cli/src/lifecycles/prerender.js @@ -95,6 +95,7 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { .filter(resource => resource.type === 'script') .map(resource => resource.sourcePathURL.href); + // TODO we should probably do this in the HTML plugin body = await new Promise((resolve, reject) => { pool.runTask({ executeModuleUrl: workerPrerender.executeModuleUrl.href, @@ -102,7 +103,7 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { compilation: JSON.stringify(compilation), page: JSON.stringify(page), prerender: true, - htmlContents: body, + htmlContents: body.replace(/</g, 'greenwood-custom-left-bracket'), scripts: JSON.stringify(scripts) }, (err, result) => { if (err) { @@ -117,6 +118,8 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { body = body.replace('', ssrContents); } + body = body.replace(/greenwood-custom-left-bracket/g, '<'); + await createOutputDirectory(route, new URL(scratchUrl.href.replace('index.html', ''))); await fs.writeFile(scratchUrl, body); diff --git a/packages/cli/test/cases/build.config.prerender-markdown/build.config.prerender-markdown.spec.js b/packages/cli/test/cases/build.config.prerender-markdown/build.config.prerender-markdown.spec.js new file mode 100644 index 000000000..08171dcd5 --- /dev/null +++ b/packages/cli/test/cases/build.config.prerender-markdown/build.config.prerender-markdown.spec.js @@ -0,0 +1,73 @@ +/* + * Use Case + * Run Greenwood with custom markdown content and prerendering enabled with WCC. + * + * User Result + * Should generate a bare bones Greenwood build and in particular make sure HTML entities are preserved. + * + * User Command + * greenwood build + * + * User Config + * { + * prerender: true + * } + * + * User Workspace + * src/ + * components/ + * ctc-block.js + * pages/ + * index.md + */ +import fs from 'fs/promises'; +import path from 'path'; +import chai from 'chai'; +import { runSmokeTest } from '../../../../../test/smoke-test.js'; +import { getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { Runner } from 'gallinago'; +import { fileURLToPath, URL } from 'url'; + +const expect = chai.expect; + +// https://github.com/ProjectEvergreen/greenwood/issues/1375 +describe('Build Greenwood With: ', function() { + const LABEL = 'Markdown with prerendering and HTML entities'; + const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js'); + const outputPath = fileURLToPath(new URL('.', import.meta.url)); + let runner; + + before(function() { + this.context = { + publicDir: path.join(outputPath, 'public') + }; + runner = new Runner(); + }); + + describe(LABEL, function() { + before(function() { + runner.setup(outputPath); + runner.runCommand(cliPath, 'build'); + }); + + runSmokeTest(['public', 'index'], LABEL); + + describe('Markdown Rendering', function() { + let html; + + before(async function() { + html = await fs.readFile(path.resolve(this.context.publicDir, 'index.html'), 'utf-8'); + }); + + it('should correctly render out code fences with HTML entities preserved', function() { + expect(html).to.contain('<x-card>'); + }); + }); + + }); + + after(function() { + runner.teardown(getOutputTeardownFiles(outputPath)); + }); + +}); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.prerender-markdown/greenwood.config.js b/packages/cli/test/cases/build.config.prerender-markdown/greenwood.config.js new file mode 100644 index 000000000..8dc4be464 --- /dev/null +++ b/packages/cli/test/cases/build.config.prerender-markdown/greenwood.config.js @@ -0,0 +1,3 @@ +export default { + prerender: true +}; \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.prerender-markdown/src/components/x-ctc.js b/packages/cli/test/cases/build.config.prerender-markdown/src/components/x-ctc.js new file mode 100644 index 000000000..b86b693ed --- /dev/null +++ b/packages/cli/test/cases/build.config.prerender-markdown/src/components/x-ctc.js @@ -0,0 +1,5 @@ +export default class CopyToClipboardBlock extends HTMLElement { + +} + +customElements.define('x-ctc', CopyToClipboardBlock); \ No newline at end of file diff --git a/packages/cli/test/cases/build.config.prerender-markdown/src/pages/index.md b/packages/cli/test/cases/build.config.prerender-markdown/src/pages/index.md new file mode 100644 index 000000000..3eef5117f --- /dev/null +++ b/packages/cli/test/cases/build.config.prerender-markdown/src/pages/index.md @@ -0,0 +1,35 @@ +--- +imports: + - /components/x-ctc.js +--- + +## Server Rendering + +You will need to use version <= 20.6.0. + + + + ```js + import "../components/card/card.js"; // + + export default class UsersPage extends HTMLElement { + async connectedCallback() { + const users = await fetch("https://www.example.com/api/users").then((resp) => resp.json()); + const html = users + .map((user) => { + const { name, imageUrl } = user; + return ` + +

${name}

+ ${name} +
+ `; + }) + .join(""); + + this.innerHTML = html; + } + } + ``` + +
\ No newline at end of file diff --git a/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js b/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js index 5b774e0bd..b43258fd3 100644 --- a/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js +++ b/packages/cli/test/cases/build.default.markdown/build.default.markdown.spec.js @@ -1,9 +1,9 @@ /* * Use Case - * Run Greenwood with custom markdown preset in greenwood config. + * Run Greenwood with markdown content. * * User Result - * Should generate a bare bones Greenwood build. (same as build.default.spec.js) with custom markdown and rehype links + * Should generate a bare bones Greenwood build with markdown correctly transformed. * * User Command * greenwood build @@ -12,7 +12,9 @@ * None * * User Workspace - * Greenwood default + * src/ + * pages/ + * index.md */ import { JSDOM } from 'jsdom'; import path from 'path'; From 515ef860eb8486bfe6630bd3acd9f2135611e956 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Thu, 9 Jan 2025 13:43:54 -0500 Subject: [PATCH 2/4] move HTML entities preservation to standard html plugin --- packages/cli/src/lifecycles/prerender.js | 5 +---- .../plugins/resource/plugin-standard-html.js | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js index 2eaff793e..785c95d61 100644 --- a/packages/cli/src/lifecycles/prerender.js +++ b/packages/cli/src/lifecycles/prerender.js @@ -95,7 +95,6 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { .filter(resource => resource.type === 'script') .map(resource => resource.sourcePathURL.href); - // TODO we should probably do this in the HTML plugin body = await new Promise((resolve, reject) => { pool.runTask({ executeModuleUrl: workerPrerender.executeModuleUrl.href, @@ -103,7 +102,7 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { compilation: JSON.stringify(compilation), page: JSON.stringify(page), prerender: true, - htmlContents: body.replace(/</g, 'greenwood-custom-left-bracket'), + htmlContents: body, scripts: JSON.stringify(scripts) }, (err, result) => { if (err) { @@ -118,8 +117,6 @@ async function preRenderCompilationWorker(compilation, workerPrerender) { body = body.replace('', ssrContents); } - body = body.replace(/greenwood-custom-left-bracket/g, '<'); - await createOutputDirectory(route, new URL(scratchUrl.href.replace('index.html', ''))); await fs.writeFile(scratchUrl, body); diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index ba0c9d39e..1ab52600e 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -159,13 +159,28 @@ class StandardHtmlResource extends ResourceInterface { }); } + // https://github.com/ProjectEvergreen/greenwood/issues/1375 + async shouldIntercept(url) { + const { pathname } = url; + const matchingRoute = this.compilation.graph.find((node) => node.route === pathname) || {}; + + return matchingRoute?.pageHref?.endsWith(this.extensions[1]) && this.compilation.config.prerender && process.env.__GWD_COMMAND__ === 'build'; // eslint-disable-line no-underscore-dangle + } + + async intercept(url, request, response) { + const body = await response.text(); + + return new Response(body.replace(/</g, 'greenwood-custom-left-bracket')); + } + async shouldOptimize(url, response) { return response.headers.get('Content-Type')?.indexOf(this.contentType) >= 0; } async optimize(url, response) { - const { optimization, basePath } = this.compilation.config; + const { optimization, basePath, prerender } = this.compilation.config; const { pathname } = url; + const matchingRoute = this.compilation.graph.find((node) => node.route === pathname) || {}; const pageResources = this.compilation.graph.find(page => page.route === pathname).resources; let body = await response.text(); @@ -245,6 +260,10 @@ class StandardHtmlResource extends ResourceInterface { } } + if (matchingRoute?.pageHref?.endsWith(this.extensions[1]) && prerender) { + body = body.replace(/greenwood-custom-left-bracket/g, '<'); + } + return new Response(body); } } From 587dd1a9fbdf29b9622759982536bbda7e439cb9 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 20 Jan 2025 17:35:53 -0500 Subject: [PATCH 3/4] test against latest version of WCC --- package.json | 4 +- .../plugins/resource/plugin-standard-html.js | 21 +- patches/wc-compiler+0.15.1.patch | 491 ++++++++++++++++++ yarn.lock | 120 ++++- 4 files changed, 613 insertions(+), 23 deletions(-) create mode 100644 patches/wc-compiler+0.15.1.patch diff --git a/package.json b/package.json index 089a26c4d..b1e6c873e 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "test:loaders": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 node --import $(pwd)/test/test-register.js ./node_modules/mocha/bin/mocha \"./packages/**/**/*.spec.js\"", "test:loaders:win": "cross-env BROWSERSLIST_IGNORE_OLD_DATA=true __GWD_ROLLUP_MODE__=strict NODE_NO_WARNINGS=1 node --import file:\\\\%cd%\\test\\test-register.js ./node_modules/mocha/bin/mocha --exclude \"./packages/init/test/cases/**\" \"./packages/**/**/*.spec.js\"", "test:tdd": "yarn test --watch", - "lint": "ls-lint && eslint" + "lint": "ls-lint && eslint", + "postinstall": "patch-package" }, "devDependencies": { "@babel/core": "^7.24.4", @@ -46,6 +47,7 @@ "jsdom": "^16.5.0", "lerna": "^3.16.4", "mocha": "^9.1.3", + "patch-package": "8.0.0", "rimraf": "^5.0.10" } } diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js index 1ab52600e..ba0c9d39e 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-html.js +++ b/packages/cli/src/plugins/resource/plugin-standard-html.js @@ -159,28 +159,13 @@ class StandardHtmlResource extends ResourceInterface { }); } - // https://github.com/ProjectEvergreen/greenwood/issues/1375 - async shouldIntercept(url) { - const { pathname } = url; - const matchingRoute = this.compilation.graph.find((node) => node.route === pathname) || {}; - - return matchingRoute?.pageHref?.endsWith(this.extensions[1]) && this.compilation.config.prerender && process.env.__GWD_COMMAND__ === 'build'; // eslint-disable-line no-underscore-dangle - } - - async intercept(url, request, response) { - const body = await response.text(); - - return new Response(body.replace(/</g, 'greenwood-custom-left-bracket')); - } - async shouldOptimize(url, response) { return response.headers.get('Content-Type')?.indexOf(this.contentType) >= 0; } async optimize(url, response) { - const { optimization, basePath, prerender } = this.compilation.config; + const { optimization, basePath } = this.compilation.config; const { pathname } = url; - const matchingRoute = this.compilation.graph.find((node) => node.route === pathname) || {}; const pageResources = this.compilation.graph.find(page => page.route === pathname).resources; let body = await response.text(); @@ -260,10 +245,6 @@ class StandardHtmlResource extends ResourceInterface { } } - if (matchingRoute?.pageHref?.endsWith(this.extensions[1]) && prerender) { - body = body.replace(/greenwood-custom-left-bracket/g, '<'); - } - return new Response(body); } } diff --git a/patches/wc-compiler+0.15.1.patch b/patches/wc-compiler+0.15.1.patch new file mode 100644 index 000000000..e8c028920 --- /dev/null +++ b/patches/wc-compiler+0.15.1.patch @@ -0,0 +1,491 @@ +diff --git a/node_modules/wc-compiler/src/dom-shim.js b/node_modules/wc-compiler/src/dom-shim.js +index be289a3..a53190d 100644 +--- a/node_modules/wc-compiler/src/dom-shim.js ++++ b/node_modules/wc-compiler/src/dom-shim.js +@@ -1,3 +1,57 @@ ++/* eslint-disable no-warning-comments */ ++ ++import { parse, parseFragment, serialize } from 'parse5'; ++ ++export function getParse(html) { ++ return html.indexOf('') >= 0 || html.indexOf('') >= 0 || html.indexOf('') >= 0 ++ ? parse ++ : parseFragment; ++} ++ ++function isShadowRoot(element) { ++ return Object.getPrototypeOf(element).constructor.name === 'ShadowRoot'; ++} ++ ++function deepClone(obj, map = new WeakMap()) { ++ if (obj === null || typeof obj !== 'object') { ++ return obj; ++ } ++ ++ if (typeof obj === 'function') { ++ const clonedFn = obj.bind({}); ++ Object.assign(clonedFn, obj); ++ return clonedFn; ++ } ++ ++ if (map.has(obj)) { ++ return map.get(obj); ++ } ++ ++ const result = Array.isArray(obj) ? [] : {}; ++ map.set(obj, result); ++ ++ for (const key of Object.keys(obj)) { ++ result[key] = deepClone(obj[key], map); ++ } ++ ++ return result; ++} ++ ++// Creates an empty parse5 element without the parse5 overhead resulting in better performance ++function getParse5ElementDefaults(element, tagName) { ++ return { ++ addEventListener: noop, ++ attrs: [], ++ parentNode: element.parentNode, ++ childNodes: [], ++ nodeName: tagName, ++ tagName: tagName, ++ namespaceURI: 'http://www.w3.org/1999/xhtml', ++ // eslint-disable-next-line no-extra-parens ++ ...(tagName === 'template' ? { content: { nodeName: '#document-fragment', childNodes: [] } } : {}) ++ }; ++} ++ + function noop() { } + + // https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet +@@ -19,13 +73,81 @@ class EventTarget { + // EventTarget <- Node + // TODO should be an interface? + class Node extends EventTarget { +- // eslint-disable-next-line ++ constructor() { ++ super(); ++ // Parse5 properties ++ this.attrs = []; ++ this.parentNode = null; ++ this.childNodes = []; ++ this.nodeName = ''; ++ } ++ + cloneNode(deep) { +- return this; ++ return deep ? deepClone(this) : Object.assign({}, this); + } + + appendChild(node) { +- this.innerHTML = this.innerHTML ? this.innerHTML += node.innerHTML : node.innerHTML; ++ const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes; ++ ++ if (node.parentNode) { ++ node.parentNode?.removeChild?.(node); ++ } ++ ++ if (node.nodeName === 'template') { ++ if (isShadowRoot(this) && this.mode) { ++ node.attrs = [{ name: 'shadowrootmode', value: this.mode }]; ++ childNodes.push(node); ++ node.parentNode = this; ++ } else { ++ this.childNodes = [...this.childNodes, ...node.content.childNodes]; ++ } ++ } else if (node instanceof DocumentFragment) { ++ this.childNodes = [...this.childNodes, ...node.childNodes]; ++ } else { ++ childNodes.push(node); ++ node.parentNode = this; ++ } ++ ++ return node; ++ } ++ ++ removeChild(node) { ++ const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes; ++ if (!childNodes || !childNodes.length) { ++ return null; ++ } ++ ++ const index = childNodes.indexOf(node); ++ if (index === -1) { ++ return null; ++ } ++ ++ childNodes.splice(index, 1); ++ node.parentNode = null; ++ ++ return node; ++ } ++ ++ get textContent() { ++ if (this.nodeName === '#text') { ++ return this.value || ''; ++ } ++ ++ return this.childNodes ++ .map((child) => child.nodeName === '#text' ? child.value : child.textContent) ++ .join(''); ++ } ++ ++ set textContent(value) { ++ this.childNodes = []; ++ ++ if (value) { ++ const textNode = new Node(); ++ textNode.nodeName = '#text'; ++ textNode.value = value; ++ textNode.parentNode = this; ++ this.childNodes.push(textNode); ++ } + } + } + +@@ -34,33 +156,44 @@ class Node extends EventTarget { + class Element extends Node { + constructor() { + super(); +- this.shadowRoot = null; +- this.innerHTML = ''; +- this.attributes = {}; + } + + attachShadow(options) { + this.shadowRoot = new ShadowRoot(options); +- ++ this.shadowRoot.parentNode = this; + return this.shadowRoot; + } + +- // https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md#serialization +- // eslint-disable-next-line +- getInnerHTML() { +- return this.shadowRoot ? this.shadowRoot.innerHTML : this.innerHTML; ++ getHTML({ serializableShadowRoots = false }) { ++ return this.shadowRoot && serializableShadowRoots && this.shadowRoot.serializable ? this.shadowRoot.innerHTML : ''; + } + +- setAttribute(name, value) { +- this.attributes[name] = value; ++ get innerHTML() { ++ const childNodes = (this.nodeName === 'template' ? this.content : this).childNodes; ++ return childNodes ? serialize({ childNodes }) : ''; + } + +- getAttribute(name) { +- return this.attributes[name]; ++ set innerHTML(html) { ++ (this.nodeName === 'template' ? this.content : this).childNodes = getParse(html)(html).childNodes; + } + + hasAttribute(name) { +- return !!this.attributes[name]; ++ return this.attrs.some((attr) => attr.name === name); ++ } ++ ++ getAttribute(name) { ++ const attr = this.attrs.find((attr) => attr.name === name); ++ return attr ? attr.value : null; ++ } ++ ++ setAttribute(name, value) { ++ const attr = this.attrs?.find((attr) => attr.name === name); ++ ++ if (attr) { ++ attr.value = value; ++ } else { ++ this.attrs?.push({ name, value }); ++ } + } + } + +@@ -75,7 +208,7 @@ class Document extends Node { + return new HTMLTemplateElement(); + + default: +- return new HTMLElement(); ++ return new HTMLElement(tagName); + + } + } +@@ -88,6 +221,10 @@ class Document extends Node { + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement + // EventTarget <- Node <- Element <- HTMLElement + class HTMLElement extends Element { ++ constructor(tagName) { ++ super(); ++ Object.assign(this, getParse5ElementDefaults(this, tagName)); ++ } + connectedCallback() { } + } + +@@ -100,9 +237,18 @@ class DocumentFragment extends Node { } + class ShadowRoot extends DocumentFragment { + constructor(options) { + super(); +- this.mode = options.mode || 'closed'; ++ this.mode = options.mode ?? 'closed'; ++ this.serializable = options.serializable ?? false; + this.adoptedStyleSheets = []; + } ++ ++ get innerHTML() { ++ return this.childNodes?.[0]?.content?.childNodes ? serialize({ childNodes: this.childNodes[0].content.childNodes }) : ''; ++ } ++ ++ set innerHTML(html) { ++ this.childNodes = getParse(html)(``).childNodes; ++ } + } + + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement +@@ -110,22 +256,11 @@ class ShadowRoot extends DocumentFragment { + class HTMLTemplateElement extends HTMLElement { + constructor() { + super(); +- this.content = new DocumentFragment(); +- } +- +- // TODO open vs closed shadow root +- set innerHTML(html) { +- if (this.content) { +- this.content.innerHTML = ` +- +- `; +- } +- } +- +- get innerHTML() { +- return this.content && this.content.innerHTML ? this.content.innerHTML : undefined; ++ // Gets element defaults for template element instead of parsing a ++ // with parse5. Results in better performance ++ // when creating templates ++ Object.assign(this, getParse5ElementDefaults(this, 'template')); ++ this.content.cloneNode = this.cloneNode.bind(this); + } + } + +@@ -156,4 +291,5 @@ globalThis.addEventListener = globalThis.addEventListener ?? noop; + globalThis.document = globalThis.document ?? new Document(); + globalThis.customElements = globalThis.customElements ?? new CustomElementsRegistry(); + globalThis.HTMLElement = globalThis.HTMLElement ?? HTMLElement; ++globalThis.DocumentFragment = globalThis.DocumentFragment ?? DocumentFragment; + globalThis.CSSStyleSheet = globalThis.CSSStyleSheet ?? CSSStyleSheet; +\ No newline at end of file +diff --git a/node_modules/wc-compiler/src/wcc.js b/node_modules/wc-compiler/src/wcc.js +index 68c7a2e..d394f17 100644 +--- a/node_modules/wc-compiler/src/wcc.js ++++ b/node_modules/wc-compiler/src/wcc.js +@@ -1,39 +1,15 @@ + /* eslint-disable max-depth */ + // this must come first +-import './dom-shim.js'; ++import { getParse } from './dom-shim.js'; + + import * as acorn from 'acorn'; + import * as walk from 'acorn-walk'; + import { generate } from 'astring'; + import { getParser, parseJsx } from './jsx-loader.js'; +-import { parse, parseFragment, serialize } from 'parse5'; ++import { serialize } from 'parse5'; + import { transform } from 'sucrase'; + import fs from 'fs'; + +-// https://developer.mozilla.org/en-US/docs/Glossary/Void_element +-const VOID_ELEMENTS = [ +- 'area', +- 'base', +- 'br', +- 'col', +- 'embed', +- 'hr', +- 'img', +- 'input', +- 'link', +- 'meta', +- 'param', // deprecated +- 'source', +- 'track', +- 'wbr' +-]; +- +-function getParse(html) { +- return html.indexOf('') >= 0 || html.indexOf('') >= 0 || html.indexOf('') >= 0 +- ? parse +- : parseFragment; +-} +- + function isCustomElementDefinitionNode(node) { + const { expression } = node; + +@@ -45,7 +21,7 @@ function isCustomElementDefinitionNode(node) { + async function renderComponentRoots(tree, definitions) { + for (const node of tree.childNodes) { + if (node.tagName && node.tagName.indexOf('-') > 0) { +- const { tagName } = node; ++ const { attrs, tagName } = node; + + if (definitions[tagName]) { + const { moduleURL } = definitions[tagName]; +@@ -53,31 +29,35 @@ async function renderComponentRoots(tree, definitions) { + + if (elementInstance) { + const hasShadow = elementInstance.shadowRoot; +- const elementHtml = hasShadow +- ? elementInstance.getInnerHTML({ includeShadowRoots: true }) +- : elementInstance.innerHTML; +- const elementTree = parseFragment(elementHtml); +- const hasLight = elementTree.childNodes > 0; +- +- node.childNodes = node.childNodes.length === 0 && hasLight && !hasShadow +- ? elementTree.childNodes +- : hasShadow +- ? [...elementTree.childNodes, ...node.childNodes] +- : elementTree.childNodes; ++ ++ node.childNodes = hasShadow ++ ? [...elementInstance.shadowRoot.childNodes, ...node.childNodes] ++ : elementInstance.childNodes; + } else { + console.warn(`WARNING: customElement <${tagName}> detected but not serialized. You may not have exported it.`); + } + } else { + console.warn(`WARNING: customElement <${tagName}> is not defined. You may not have imported it.`); + } ++ ++ attrs.forEach((attr) => { ++ if (attr.name === 'hydrate') { ++ definitions[tagName].hydrate = attr.value; ++ } ++ }); ++ + } + + if (node.childNodes && node.childNodes.length > 0) { + await renderComponentRoots(node, definitions); + } + ++ if (node.shadowRoot && node.shadowRoot.childNodes?.length > 0) { ++ await renderComponentRoots(node.shadowRoot, definitions); ++ } ++ + // does this only apply to `