UNPKG

@trifrost/core

Version:

Blazingly fast, runtime-agnostic server framework for modern edge and node environments

241 lines (235 loc) 9.41 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StyleEngine = exports.OBSERVER = exports.SHARD = exports.PRIME = void 0; const Style_1 = require("./Style"); const nonce_1 = require("../ctx/nonce"); const util_1 = require("../script/util"); const Generic_1 = require("../../../utils/Generic"); exports.PRIME = 'data-tfs-p'; exports.SHARD = 'data-tfs-s'; exports.OBSERVER = (0, util_1.atomicMinify)(`(function(){ const cn = new Set(); let prime = document.querySelector("style[${exports.PRIME}]"); if (!prime) return; /* Scan primary for known classes */ const cnr = /\\.([a-zA-Z0-9_-]+)[,{]/g; let m; while ((m = cnr.exec(prime.textContent))) cn.add(m[1]); function c (n, p) { if ( n.nodeType === Node.ELEMENT_NODE && n.tagName === "STYLE" && n.hasAttribute("${exports.SHARD}") ) { const s = n.getAttribute("${exports.SHARD}"); if (!s || cn.has(s)) return n.remove(); cn.add(s); if (n.textContent) p.add(n.textContent); return n.remove(); } n.childNodes?.forEach(k => c(k, p)); } function b() { const o = new MutationObserver(e => { /* Scan mutations for shard style blocks */ const pp = new Set(); for (let i = 0; i < e.length; i++) { for (let y = 0; y < e[i].addedNodes.length; y++) { c(e[i].addedNodes[y], pp); } } /* Build a prime shard and append after prime */ if (pp.size) { const nN = document.createElement("style"); const nS = window.$${nonce_1.NONCEMARKER}; if (typeof nS === "string" && nS.length) nN.setAttribute("nonce", nS); nN.setAttribute("${exports.PRIME}s", ""); nN.textContent = [...pp.values()].join(""); prime.after(nN); } }); o.observe(document.body, {childList: true, subtree: true}); } if (document.body) { b(); } else { document.addEventListener("DOMContentLoaded", b); } })();`); class StyleEngine { /* Global hash register of hashes to their rules */ rules = {}; /* Order of rule injection */ order = new Set(); /* Whether or not style injection was explicitly disabled for this engine */ disabled = false; /* Mount path for root styles */ mount_path = null; cache = new Map(); /** * Set the disabled state of this engine. In disabled mode we will not flush non-mounted styles */ setDisabled(val) { this.disabled = !!val; } /** * Register a rule (base or media) under a known class name * * @param {string} rule - Raw CSS declaration (e.g., 'color:red') * @param {string} name - Deterministic class name to register under (usually the output of StyleEngine.hash) * @param {StyleEngineRegisterOptions} opts - Optional context including media query and selector */ register(rule, name, opts) { if (typeof rule !== 'string' || !rule.length || (opts.selector !== undefined && (typeof opts.selector !== 'string' || !opts.selector.length) && opts.selector !== null)) return; const { query, selector } = opts; const key = name || (rule.startsWith('@keyframes') ? rule.slice(11).split('{', 1)[0].trim() : (0, Generic_1.djb2Hash)(rule)); let entry = this.rules[key]; if (!entry) { entry = { base: new Set(), media: {} }; this.rules[key] = entry; if (!this.order.has(key)) this.order.add(key); } if (!query) { if (rule[0] === '@') { entry.base.add(rule.trim()); } else { const prefix = selector !== null ? (selector ?? '.' + name) : ''; entry.base.add(prefix + (selector === null ? rule.trim() : `{${rule.trim()}}`)); } } else { if (!entry.media[query]) entry.media[query] = new Set(); const prefix = selector !== null ? (selector ?? '.' + name) : ''; entry.media[query].add(prefix ? prefix + '{' + rule.trim() + '}' : rule.trim()); } } /** * Flush all collected styles into a single <style> tag */ flush(opts = {}) { const n_nonce = (0, nonce_1.nonce)(); const order = this.order.values(); switch (opts?.mode) { case 'style': case 'file': case 'prime': { let out = ''; const media = {}; for (const name of order) { const entry = this.rules[name]; if (entry.base) out += [...entry.base].join(''); if (entry.media) { for (const query in entry.media) { (media[query] ??= []).push(...entry.media[query]); } } } for (const query in media) { out += query + '{' + media[query].join('') + '}'; } if (opts.mode === 'file') { return out; } else if (opts.mode === 'style') { if (!out) return ''; return n_nonce ? `<style nonce="${n_nonce}">${out}</style>` : `<style>${out}</style>`; } else { const observer = (n_nonce ? '<script nonce="' + n_nonce + '">' : '<script>') + exports.OBSERVER + '</script>'; return n_nonce ? `<style nonce="${n_nonce}" ${exports.PRIME}>${out}</style>${observer}` : `<style ${exports.PRIME}>${out}</style>${observer}`; } } case 'shards': { const shards = []; for (const name of order) { const entry = this.rules[name]; // Build the style block let content = ''; if (entry.base) content += [...entry.base].join(''); if (entry.media) { for (const query in entry.media) { content += query + '{' + [...entry.media[query].values()].join('') + '}'; } } if (content) { const style = n_nonce ? `<style ${exports.SHARD}="${name}" nonce="${n_nonce}">${content}</style>` : `<style ${exports.SHARD}="${name}">${content}</style>`; shards.push(style); } } return shards.join(''); } default: return ''; } } /** * Replace the style marker with collected styles in the rendered HTML * * @param {string} html - HTML string containing the marker or needing prepended styles */ inject(html) { /* If disabled, return */ if (this.disabled) return typeof html === 'string' ? html.replaceAll(Style_1.MARKER, '') : ''; /** * On full-page render we work with a single block, * On fragment render we work with individual shards per rule hash * * This allows a global mutation observer to 'filter' shards out when they arrive to only include the ones * that matter. */ const mode = typeof html !== 'string' || !html.length ? 'style' : html.startsWith('<!DOCTYPE') || html.startsWith('<html') ? 'prime' : 'shards'; // eslint-disable-line prettier/prettier if (mode === 'style') return this.flush({ mode }); /* Get mount styles */ let mount_styles = ''; if (this.mount_path && mode === 'prime') { const n_nonce = (0, nonce_1.nonce)(); if (n_nonce) mount_styles = '<link rel="stylesheet" nonce="' + n_nonce + '" href="' + this.mount_path + '">'; else mount_styles = '<link rel="stylesheet" href="' + this.mount_path + '">'; } const styles = this.flush({ mode }); /* Inject at marker */ const marker_idx = html.indexOf(Style_1.MARKER); if (marker_idx >= 0) { const before = html.slice(0, marker_idx); const after = html.slice(marker_idx + Style_1.MARKER.length).replaceAll(Style_1.MARKER, ''); return before + mount_styles + styles + after; } /* If in shard/fragment mode */ if (mode === 'shards') return html + styles; return (0, Generic_1.injectBefore)(html, mount_styles + styles, ['</head>', '</body>']); } /** * Clears all internal state */ reset() { this.rules = {}; this.order = new Set(); } /** * Sets mount path for as-file renders of root styles * * @param {string} path - Mount path for client root styles */ setMountPath(path) { this.mount_path = path; } } exports.StyleEngine = StyleEngine;