/** * Represents a style object on a custom element. */ export interface IStyleObject { cssText: string; } /** * Represents a constructor for a custom element that may have a static `styles` property. */ export interface ICustomElementConstructor extends Function { styles?: IStyleObject[]; } declare var document: Document; /** * Serializes shadow-DOM-based elements into light DOM by: * 1. Moving slotted children out of their elements. * 2. Converting shadow-root CSS to scoping-based CSS. * 3. Appending styles and elements back onto the node. * * @param rootNode The root node under which all child nodes will be serialized. */ export function serializeFunction(rootNode: Node): void { /** * Instead of a UUID, we use a globally incrementing counter for stable, * predictable IDs across multiple serializations of the same tree. */ let globalIdCounter = 0; const generateStableId = (): string => { globalIdCounter++; // Feel free to adjust the prefix/suffix as you like return `unix-${globalIdCounter}`; }; /** * Prepend the generated ID class to CSS in order to emulate :host scoping. */ const prependCss = (uuidID: string, styleTemplate: string): string => { // Ensure there's at least one :host, so the user code typically expects scoping if (!styleTemplate.includes(':host')) { styleTemplate = `:host {}\n\n${styleTemplate}`; } // Replace patterns that should be placed under the .uuidID scope styleTemplate = styleTemplate.replace(/}[ \t\n]+\./g, `}\n\n.${uuidID} .`); styleTemplate = styleTemplate.replace(/}[ \t\n]+\*/g, `}\n\n.${uuidID} *`); styleTemplate = styleTemplate.replace(/\(\[/g, `[`); styleTemplate = styleTemplate.replace(/\]\)/g, `]`); // Replace :host with .uuidID styleTemplate = styleTemplate.replace(/:host/g, `.${uuidID}`); styleTemplate = styleTemplate.replace(/{[ \t\n]+\./g, `{\n\n.${uuidID} .`); styleTemplate = styleTemplate.replace(/}[ \t\n]+img/g, `}\n\n.${uuidID} img`); styleTemplate = styleTemplate.replace(/}[ \t\n]+div/g, `}\n\n.${uuidID} div`); return styleTemplate; }; // Keep track of visited nodes to avoid loops const visitedNodes = new WeakSet(); /** * Recursively serializes a node, moving shadow DOM content into light DOM. * * @param nodeArg The node to serialize. */ function serializeNode(nodeArg: HTMLElement | Node): void { // Prevent re-serializing the same node if (visitedNodes.has(nodeArg)) { return; } visitedNodes.add(nodeArg); // If the node has a shadowRoot, move everything into light DOM if ( nodeArg instanceof HTMLElement && nodeArg.shadowRoot && nodeArg.shadowRoot instanceof ShadowRoot ) { // Mark node for SSR nodeArg.setAttribute('smartssr', 'yes'); // Generate a stable ID for CSS scoping const nodeId = generateStableId(); nodeArg.classList.add(nodeId); // Move slotted nodes from to the light DOM const slots = nodeArg.shadowRoot.querySelectorAll('slot'); const slotsForMove: HTMLSlotElement[] = Array.from(slots); for (const slot of slotsForMove) { const slottedLightNodesForMove = slot.assignedNodes(); slottedLightNodesForMove.forEach((lightNode) => { if (slot.parentNode) { slot.parentNode.insertBefore(lightNode, slot); } }); } // Prepare nodes to append after transformations const nodesToAppend: Node[] = []; // Some frameworks store static styles in a static `styles` property on the constructor const elementConstructor = nodeArg.constructor as ICustomElementConstructor; if (Array.isArray(elementConstructor.styles)) { for (const styleObj of elementConstructor.styles) { const styleTag = document.createElement('style'); styleTag.textContent = prependCss(nodeId, styleObj.cssText); nodesToAppend.push(styleTag); } } // Convert existing shadow DOM childNodes nodeArg.shadowRoot.childNodes.forEach((childNode: ChildNode) => { if (childNode instanceof HTMLElement) { // If it's a