import { REMIX_UI_STYLE_LAYER } from './layers.ts' type RuleEntry = { count: number; index: number } export type ServerStyleSource = ParentNode | Iterable export interface StyleManager { insert(className: string, rule: string): void remove(className: string): void has(className: string): boolean getGeneration(): number reset(): void adoptServerStyles(source: ServerStyleSource): Set replaceServerStyles(source: ServerStyleSource): void selectors(): IterableIterator dispose(): void } const SERVER_STYLE_SELECTOR = 'style[data-rmx]' function getStyleLayerName(className: string, layer: string = REMIX_UI_STYLE_LAYER): string { return `${layer}.${className}` } function compareNodesInDocumentOrder(a: Node, b: Node): number { if (a === b) return 0 let position = a.compareDocumentPosition(b) if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1 if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1 return 0 } function isParentNode(value: ServerStyleSource): value is ParentNode { return 'querySelectorAll' in value } function collectServerStyleTagsFromNode(node: Node, into: Set): void { if (isHtmlStyleElement(node) && node.matches(SERVER_STYLE_SELECTOR)) { into.add(node) return } if ( !(node instanceof Element) && !(node instanceof Document) && !(node instanceof DocumentFragment) ) { return } let nested = node.querySelectorAll?.(SERVER_STYLE_SELECTOR) ?? [] for (let i = 0; i < nested.length; i++) { let el = nested[i] if (isHtmlStyleElement(el)) { into.add(el) } } } function collectServerStyleTags(source: ServerStyleSource): HTMLStyleElement[] { let styles = new Set() if (isParentNode(source)) { collectServerStyleTagsFromNode(source as unknown as Node, styles) } else { for (let node of source) { collectServerStyleTagsFromNode(node, styles) } } return Array.from(styles).sort(compareNodesInDocumentOrder) } function isHtmlStyleElement(node: unknown): node is HTMLStyleElement { return typeof node === 'object' && node !== null && node instanceof HTMLStyleElement } function getStyleSelector(styleEl: HTMLStyleElement): string | null { let selector = styleEl.getAttribute('data-rmx')?.trim() return selector ? selector : null } export function createStyleManager(layer: string = REMIX_UI_STYLE_LAYER): StyleManager { let stylesheet: CSSStyleSheet | null = null let generation = 0 // Track usage count and rule index per className // Using an object to track both count and index together let ruleMap = new Map() // Selectors currently held by a server-style adoption ref. We track this // separately from `ruleMap` so `replaceServerStyles` can release only the // adoption refs of selectors absent from the next page, without disturbing // selectors that exist solely because a client-side css mixin inserted them // (e.g. transient UI state that the server never rendered). let adoptedSelectors = new Set() function getStylesheet(): CSSStyleSheet { if (!stylesheet) { stylesheet = new CSSStyleSheet() document.adoptedStyleSheets.push(stylesheet) } return stylesheet } function removeStylesheet() { if (!stylesheet) return document.adoptedStyleSheets = Array.from(document.adoptedStyleSheets).filter( (s) => s !== stylesheet, ) stylesheet = null } function clearStylesheet() { if (!stylesheet) return for (let i = stylesheet.cssRules.length - 1; i >= 0; i--) { stylesheet.deleteRule(i) } } function adoptServerStyleTag(styleEl: HTMLStyleElement): string | undefined { let selector = getStyleSelector(styleEl) if (!selector) return undefined if (ruleMap.has(selector)) { // Already tracked. Take an adoption ref if we don't already have one — // the rule may have been inserted by a client-side css mixin first and // the SSR'd style tag arrived afterwards (e.g. a streamed fragment). if (!adoptedSelectors.has(selector)) { let entry = ruleMap.get(selector)! entry.count++ adoptedSelectors.add(selector) } styleEl.remove() return selector } let cssText = styleEl.textContent?.trim() ?? '' if (cssText.length === 0) { styleEl.remove() return undefined } try { let sheet = getStylesheet() let index = sheet.cssRules.length sheet.insertRule(cssText, index) ruleMap.set(selector, { count: 1, index }) adoptedSelectors.add(selector) styleEl.remove() return selector } catch { // If adoption fails, keep the