vendor: 18 bytes, lines 1-2
1(() => { 2 // src/
2Utils.js 3 var BeaconUtils = class { 4 static getScreenWidth() { 5 return window.innerWidth || document.documentElement.clientWidth; 6 } 7 static getScreenHeight() { 8 return window.innerHeight || document.documentElement.clientHeight; 9 } 10 static isNotValidScreensize(is_mobile, threshold) { 11 const screenWidth = this.getScreenWidth(); 12 const screenHeight = this.getScreenHeight(); 13 const isNotValidForMobile = is_mobile && (screenWidth > threshold.width || screenHeight > threshold.height); 14 const isNotValidForDesktop = !is_mobile && (screenWidth < threshold.width || screenHeight < threshold.height); 15 return isNotValidForMobile || isNotValidForDesktop; 16 } 17 static isPageCached() { 18 const signature = document.documentElement.nextSibling && document.documentElement.nextSibling.data ? document.documentElement.nextSibling.data : ""; 19 return signature && signature.includes("Debug: cached"); 20 } 21 static isIntersecting(rect) { 22 return rect.bottom >= 0 && rect.right >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) && rect.left <= (window.innerWidth || document.documentElement.clientWidth); 23 } 24 static isPageScrolled() { 25 return window.pageYOffset > 0 || document.documentElement.scrollTop > 0; 26 } 27 /** 28 * Checks if an element is visible in the viewport. 29 * 30 * This method checks if the provided element is visible in the viewport by 31 * considering its display, visibility, opacity, width, and height properties. 32 * It also excludes elements with transparent text properties. 33 * It returns true if the element is visible, and false otherwise. 34 * 35 * @param {Element} element - The element to check for visibility. 36 * @returns {boolean} True if the element is visible, false otherwise. 37 */ 38 static isElementVisible(element) { 39 const style = window.getComputedStyle(element); 40 const rect = element.getBoundingClientRect(); 41 if (!style) { 42 return false; 43 } 44 if (this.hasTransparentText(element)) { 45 return false; 46 } 47 return !(style.display === "none" || style.visibility === "hidden" || style.opacity === "0" || rect.width === 0 || rect.height === 0); 48 } 49 /** 50 * Checks if an element has transparent text properties. 51 * 52 * This method checks for specific CSS properties that make text invisible, 53 * such as `color: transparent`, `color: rgba(..., 0)`, `color: hsla(..., 0)`, 54 * `color: #...00` (8-digit hex with alpha = 0), and `filter: opacity(0)`. 55 * 56 * @param {Element} element - The element to check. 57 * @returns {boolean} True if the element has transparent text properties, false otherwise. 58 */ 59 static hasTransparentText(element) { 60 const style = window.getComputedStyle(element); 61 if (!style) { 62 return false; 63 } 64 const color = style.color || ""; 65 const filter = style.filter || ""; 66 if (color === "transparent") { 67 return true; 68 } 69 const rgbaMatch = color.match(/rgba\(\d+,\s*\d+,\s*\d+,\s*0\)/); 70 if (rgbaMatch) { 71 return true; 72 } 73 const hslaMatch = color.match(/hsla\(\d+,\s*\d+%,\s*\d+%,\s*0\)/); 74 if (hslaMatch) { 75 return true; 76 } 77 const hexMatch = color.match(/#[0-9a-fA-F]{6}00/); 78 if (hexMatch) { 79 return true; 80 } 81 if (filter.includes("opacity(0)")) { 82 return true; 83 } 84 return false; 85 } 86 }; 87 var Utils_default = BeaconUtils; 88 89
89// src/BeaconLcp.js 90 var BeaconLcp = class { 91 constructor(config, logger) { 92 this.config = config; 93 this.performanceImages = []; 94 this.logger = logger; 95 } 96 async run() { 97 try { 98 const above_the_fold_images = this._generateLcpCandidates(Infinity); 99 if (above_the_fold_images) { 100 this._initWithFirstElementWithInfo(above_the_fold_images); 101 this._fillATFWithoutDuplications(above_the_fold_images); 102 } 103 } catch (err) { 104 this.errorCode = "script_error"; 105 this.logger.logMessage("Script Error: " + err); 106 } 107 } 108 _generateLcpCandidates(count) { 109 const lcpElements = document.querySelectorAll(this.config.elements); 110 if (lcpElements.length <= 0) { 111 return []; 112 } 113 const potentialCandidates = Array.from(lcpElements); 114 const topCandidates = potentialCandidates.map((element) => { 115 if ("img" === element.nodeName.toLowerCase() && "picture" === element.parentElement.nodeName.toLowerCase()) { 116 return null; 117 } 118 let rect; 119 if ("picture" === element.nodeName.toLowerCase()) { 120 const imgElement = element.querySelector("img"); 121 if (imgElement) { 122 rect = imgElement.getBoundingClientRect(); 123 } else { 124 return null; 125 } 126 } else { 127 rect = element.getBoundingClientRect(); 128 } 129 return { 130 element, 131 rect 132 }; 133 }).filter((item) => item !== null).filter((item) => { 134 return item.rect.width > 0 && item.rect.height > 0 && Utils_default.isIntersecting(item.rect) && Utils_default.isElementVisible(item.element); 135 }).map((item) => ({ 136 item, 137 area: this._getElementArea(item.rect), 138 elementInfo: this._getElementInfo(item.element) 139 })).sort((a, b) => b.area - a.area).slice(0, count); 140 return topCandidates.map((candidate) => ({ 141 element: candidate.item.element, 142 elementInfo: candidate.elementInfo 143 })); 144 } 145 _getElementArea(rect) { 146 const visibleWidth = Math.min(rect.width, (window.innerWidth || document.documentElement.clientWidth) - rect.left); 147 const visibleHeight = Math.min(rect.height, (window.innerHeight || document.documentElement.clientHeight) - rect.top); 148 return visibleWidth * visibleHeight; 149 } 150 _getElementInfo(element) { 151 const nodeName = element.nodeName.toLowerCase(); 152 const element_info = { 153 type: "", 154 src: "", 155 srcset: "", 156 sizes: "", 157 sources: [], 158 bg_set: [], 159 current_src: "" 160 }; 161 const css_bg_url_rgx = /url\(\s*?['"]?\s*?(.+?)\s*?["']?\s*?\)/ig; 162 if (nodeName === "img" && element.srcset) { 163 element_info.type = "img-srcset"; 164 element_info.src = element.src; 165 element_info.srcset = element.srcset; 166 element_info.sizes = element.sizes; 167 element_info.current_src = element.currentSrc; 168 } else if (nodeName === "img") { 169 element_info.type = "img"; 170 element_info.src = element.src; 171 element_info.current_src = element.currentSrc; 172 } else if (nodeName === "video") { 173 element_info.type = "img"; 174 const source = element.querySelector("source"); 175 element_info.src = element.poster || (source ? source.src : ""); 176 element_info.current_src = element_info.src; 177 } else if (nodeName === "svg") { 178 const imageElement = element.querySelector("image"); 179 if (imageElement) { 180 element_info.type = "img"; 181 element_info.src = imageElement.getAttribute("href") || ""; 182 element_info.current_src = element_info.src; 183 } 184 } else if (nodeName === "picture") { 185 element_info.type = "picture"; 186 const img = element.querySelector("img"); 187 element_info.src = img ? img.src : ""; 188 element_info.sources = Array.from(element.querySelectorAll("source")).map((source) => ({ 189 srcset: source.srcset || "", 190 media: source.media || "", 191 type: source.type || "", 192 sizes: source.sizes || "" 193 })); 194 } else { 195 const computed_style = window.getComputedStyle(element, null); 196 const bg_props = [ 197 computed_style.getPropertyValue("background-image"), 198 getComputedStyle(element, ":after").getPropertyValue("background-image"), 199 getComputedStyle(element, ":before").getPropertyValue("background-image") 200 ].filter((prop) => prop !== "none"); 201 if (bg_props.length === 0) { 202 return null; 203 } 204 const full_bg_prop = bg_props[0]; 205 element_info.type = "bg-img"; 206 if (full_bg_prop.includes("image-set(")) { 207 element_info.type = "bg-img-set"; 208 } 209 if (!full_bg_prop || full_bg_prop === "" || full_bg_prop.includes("data:image")) { 210 return null; 211 } 212 const matches = [...full_bg_prop.matchAll(css_bg_url_rgx)]; 213 element_info.bg_set = matches.map((m) => m[1] ? { src: m[1].trim() + (m[2] ? " " + m[2].trim() : "") } : {}); 214 if (element_info.bg_set.every((item) => item.src === "")) { 215 element_info.bg_set = matches.map((m) => m[1] ? { src: m[1].trim() } : {}); 216 } 217 if (element_info.bg_set.length <= 0) { 218 return null; 219 } 220 if (element_info.bg_set.length > 0) { 221 element_info.src = element_info.bg_set[0].src; 222 if (element_info.type === "bg-img-set") { 223 element_info.src = element_info.bg_set; 224 } 225 } 226 } 227 return element_info; 228 } 229 _initWithFirstElementWithInfo(elements) { 230 const firstElementWithInfo = elements.find((item) => { 231 return item.elementInfo !== null && (item.elementInfo.src || item.elementInfo.srcset); 232 }); 233 if (!firstElementWithInfo) { 234 this.logger.logMessage("No LCP candidate found."); 235 this.performanceImages = []; 236 return; 237 } 238 this.performanceImages = [{ 239 ...firstElementWithInfo.elementInfo, 240 label: "lcp" 241 }]; 242 } 243 _fillATFWithoutDuplications(elements) {
244 elements.forEach(({ element, elementInfo }) => { 245 if (this._isDuplicateImage(element) || !elementInfo) { 246 return; 247 } 248 this.performanceImages.push({ ...elementInfo, label: "above-the-fold" }); 249 }); 250 } 251 _isDuplicateImage(image) { 252 const elementInfo = this._getElementInfo(image); 253 if (elementInfo === null) { 254 return false; 255 } 256 const isImageOrVideo = elementInfo.type === "img" || elementInfo.type === "img-srcset" || elementInfo.type === "video"; 257 const isBgImageOrPicture = elementInfo.type === "bg-img" || elementInfo.type === "bg-img-set" || elementInfo.type === "picture"; 258 return (isImageOrVideo || isBgImageOrPicture) && this.performanceImages.some((item) => item.src === elementInfo.src); 259 } 260 getResults() { 261 return this.performanceImages; 262 } 263 }; 264 var BeaconLcp_default = BeaconLcp; 265 266
266// src/BeaconLrc.js 267 var BeaconLrc = class { 268 constructor(config, logger) { 269 this.config = config; 270 this.logger = logger; 271 this.lazyRenderElements = []; 272 } 273 async run() { 274 try { 275 const elementsInView = this._getLazyRenderElements(); 276 if (elementsInView) { 277 this._processElements(elementsInView); 278 } 279 } catch (err) { 280 this.errorCode = "script_error"; 281 this.logger.logMessage("Script Error: " + err); 282 } 283 } 284 _getLazyRenderElements() { 285 const elements = document.querySelectorAll("[data-rocket-location-hash]"); 286 const svgUseTargets = this._getSvgUseTargets(); 287 if (elements.length <= 0) { 288 return []; 289 } 290 const validElements = Array.from(elements).filter((element) => { 291 if (this._skipElement(element)) { 292 return false; 293 } 294 if (svgUseTargets.includes(element)) { 295 this.logger.logColoredMessage(`Element skipped because of SVG: ${element.tagName}`, "orange"); 296 return false; 297 } 298 return true; 299 }); 300 return validElements.map((element) => ({ 301 element, 302 depth: this._getElementDepth(element), 303 distance: this._getElementDistance(element), 304 hash: this._getLocationHash(element) 305 })); 306 } 307 _getElementDepth(element) { 308 let depth = 0; 309 let parent = element.parentElement; 310 while (parent) { 311 depth++; 312 parent = parent.parentElement; 313 } 314 return depth; 315 } 316 _getElementDistance(element) { 317 const rect = element.getBoundingClientRect(); 318 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 319 return Math.max(0, rect.top + scrollTop - Utils_default.getScreenHeight()); 320 } 321 _skipElement(element) { 322 const skipStrings = this.config.skipStrings || ["memex"]; 323 if (!element || !element.id) return false; 324 return skipStrings.some((str) => element.id.toLowerCase().includes(str.toLowerCase())); 325 } 326 _shouldSkipElement(element, exclusions) { 327 if (!element) return false; 328 for (let i = 0; i < exclusions.length; i++) { 329 const [attribute, pattern] = exclusions[i]; 330 const attributeValue = element.getAttribute(attribute); 331 if (attributeValue && new RegExp(pattern, "i").test(attributeValue)) { 332 return true; 333 } 334 } 335 return false; 336 } 337 _checkLcrConflict(element) { 338 const conflictingElements = []; 339 const computedStyle = window.getComputedStyle(element); 340 const validMargins = ["marginTop", "marginRight", "marginBottom", "marginLeft"]; 341 const negativeMargins = validMargins.some((margin) => parseFloat(computedStyle[margin]) < 0); 342 const currentElementConflicts = negativeMargins || computedStyle.contentVisibility === "auto" || computedStyle.contentVisibility === "hidden"; 343 if (currentElementConflicts) { 344 conflictingElements.push({ 345 element, 346 conflicts: [ 347 negativeMargins && "negative margin", 348 computedStyle.contentVisibility === "auto" && "content-visibility:auto", 349 computedStyle.contentVisibility === "hidden" && "content-visibility:hidden" 350 ].filter(Boolean) 351 }); 352 } 353 Array.from(element.children).forEach((child) => { 354 const childStyle = window.getComputedStyle(child); 355 const validMargins2 = ["marginTop", "marginRight", "marginBottom", "marginLeft"]; 356 const childNegativeMargins = validMargins2.some((margin) => parseFloat(childStyle[margin]) < 0); 357 const childConflicts = childNegativeMargins || childStyle.position === "absolute" || childStyle.position === "fixed"; 358 if (childConflicts) { 359 conflictingElements.push({ 360 element: child, 361 conflicts: [ 362 childNegativeMargins && "negative margin", 363 childStyle.position === "absolute" && "position:absolute", 364 childStyle.position === "fixed" && "position:fixed" 365 ].filter(Boolean) 366 }); 367 } 368 }); 369 return conflictingElements; 370 } 371 _processElements(elements) {
372 elements.forEach(({ element, depth, distance, hash }) => { 373 if (this._shouldSkipElement(element, this.config.exclusions || [])) { 374 return; 375 } 376 if ("No hash detected" === hash) { 377 return; 378 } 379 const conflicts = this._checkLcrConflict(element); 380 if (conflicts.length > 0) { 381 this.logger.logMessage("Skipping element due to conflicts:", conflicts); 382 return; 383 } 384 const can_push_hash = element.parentElement && this._getElementDistance(element.parentElement) < this.config.lrc_threshold && distance >= this.config.lrc_threshold; 385 const color = can_push_hash ? "green" : distance === 0 ? "red" : ""; 386 this.logger.logColoredMessage(`${" ".repeat(depth)}${element.tagName} (Depth: ${depth}, Distance from viewport bottom: ${distance}px)`, color); 387 this.logger.logColoredMessage(`${" ".repeat(depth)}Location hash: ${hash}`, color); 388 this.logger.logColoredMessage(`${" ".repeat(depth)}Dimensions Client Height: ${element.clientHeight}`, color); 389 if (can_push_hash) { 390 this.lazyRenderElements.push(hash); 391 this.logger.logMessage(`Element pushed with hash: ${hash}`); 392 } 393 }); 394 } 395 _getXPath(element) { 396 if (element && element.id !== "") { 397 return `//*[@id="${element.id}"]`; 398 } 399 return this._getElementXPath(element); 400 } 401 _getElementXPath(element) { 402 if (element === document.body) { 403 return "/html/body"; 404 } 405 const position = this._getElementPosition(element); 406 return `${this._getElementXPath(element.parentNode)}/${element.nodeName.toLowerCase()}[${position}]`; 407 } 408 _getElementPosition(element) { 409 let pos = 1; 410 let sibling = element.previousElementSibling; 411 while (sibling) { 412 if (sibling.nodeName === element.nodeName) { 413 pos++; 414 } 415 sibling = sibling.previousElementSibling; 416 } 417 return pos; 418 } 419 _getLocationHash(element) { 420 return element.hasAttribute("data-rocket-location-hash") ? element.getAttribute("data-rocket-location-hash") : "No hash detected"; 421 } 422 _getSvgUseTargets() { 423 const useElements = document.querySelectorAll("use"); 424 const targets = /* @__PURE__ */ new Set(); 425 useElements.forEach((use) => { 426 let parent = use.parentElement; 427 while (parent && parent !== document.body) { 428 targets.add(parent); 429 parent = parent.parentElement; 430 } 431 }); 432 return Array.from(targets); 433 } 434 getResults() { 435 return this.lazyRenderElements; 436 } 437 }; 438 var BeaconLrc_default = BeaconLrc; 439 440
440// src/BeaconPreloadFonts.js 441 var BeaconPreloadFonts = class { 442 constructor(config, logger) { 443 this.config = config; 444 this.logger = logger; 445 this.aboveTheFoldFonts = []; 446 const extensions = (Array.isArray(this.config.processed_extensions) && this.config.processed_extensions.length > 0 ? this.config.processed_extensions : ["woff", "woff2", "ttf"]).map((ext) => ext.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"); 447 this.FONT_FILE_REGEX = new RegExp(`\\.(${extensions})(\\?.*)?$`, "i"); 448 this.EXCLUDED_TAG_NAMES = /* @__PURE__ */ new Set([ 449 // Metadata/document head 450 "BASE", 451 "HEAD", 452 "LINK", 453 "META", 454 "STYLE", 455 "TITLE", 456 "SCRIPT", 457 // Media 458 "IMG", 459 "VIDEO", 460 "AUDIO", 461 "EMBED", 462 "OBJECT", 463 "IFRAME", 464 // Templating, wrappers, components, fallback 465 "NOSCRIPT", 466 "TEMPLATE", 467 "SLOT", 468 "CANVAS", 469 // Resources 470 "SOURCE", 471 "TRACK", 472 "PARAM", 473 // SVG references 474 "USE", 475 "SYMBOL", 476 // Layout work 477 "BR", 478 "HR", 479 "WBR", 480 // Obsolete/deprecated 481 "APPLET", 482 "ACRONYM", 483 "BGSOUND", 484 "BIG", 485 "BLINK", 486 "CENTER", 487 "FONT", 488 "FRAME", 489 "FRAMESET", 490 "MARQUEE", 491 "NOFRAMES", 492 "STRIKE", 493 "TT", 494 "U", 495 "XMP" 496 ]); 497 } 498 /** 499 * Checks if a URL should be excluded from external font processing based on domain exclusions. 500 * 501 * @param {string} url - The URL to check. 502 * @returns {boolean} True if the URL should be excluded, false otherwise. 503 */
504 isUrlExcludedFromExternalProcessing(url) { 505 if (!url) return false; 506 const externalFontExclusions = this.config.external_font_exclusions || []; 507 const preloadFontsExclusions = this.config.preload_fonts_exclusions || []; 508 const allExclusions = [...externalFontExclusions, ...preloadFontsExclusions]; 509 return allExclusions.some((exclusion) => url.includes(exclusion)); 510 } 511 /** 512 * Checks if a font family or URL should be excluded from preloading. 513 * 514 * This method determines if the provided font family or any of its URLs 515 * match any exclusion patterns defined in the configuration. It checks for 516 * exact matches and substring matches for both the font family and URLs. 517 * 518 * @param {string} fontFamily - The font family to check. 519 * @param {string[]} urls - Array of font file URLs to check. 520 * @returns {boolean} True if the font should be excluded, false otherwise. 521 */ 522 isExcluded(fontFamily, urls) { 523 const exclusions = this.config.preload_fonts_exclusions; 524 const exclusionsSet = new Set(exclusions); 525 if (exclusionsSet.has(fontFamily)) { 526 return true; 527 } 528 if (exclusions.some((exclusion) => fontFamily.includes(exclusion))) { 529 return true; 530 } 531 if (Array.isArray(urls) && urls.length > 0) { 532 if (urls.some((url) => exclusionsSet.has(url))) { 533 return true; 534 } 535 if (urls.some( 536 (url) => exclusions.some((exclusion) => url.includes(exclusion)) 537 )) { 538 return true; 539 } 540 } 541 return false; 542 } 543 /** 544 * Checks if an element can be styled with font-family. 545 * 546 * This method determines if the provided element's tag name is not in the list 547 * of excluded tag names that cannot be styled with font-family CSS property. 548 * 549 * @param {Element} element - The element to check. 550 * @returns {boolean} True if the element can be styled with font-family, false otherwise. 551 */ 552 canElementBeStyledWithFontFamily(element) { 553 return !this.EXCLUDED_TAG_NAMES.has(element.tagName); 554 } 555 /** 556 * Checks if an element is visible in the viewport. 557 * 558 * This method delegates to BeaconUtils.isElementVisible() for consistent 559 * visibility checking across all beacons. 560 * 561 * @param {Element} element - The element to check for visibility. 562 * @returns {boolean} True if the element is visible, false otherwise. 563 */ 564 isElementVisible(element) { 565 return Utils_default.isElementVisible(element); 566 } 567 /** 568 * Cleans a URL by removing query parameters and fragments. 569 * 570 * This method takes a URL as input, removes any query parameters and fragments, 571 * and returns the cleaned URL. 572 * 573 * @param {string} url - The URL to clean. 574 * @returns {string} The cleaned URL. 575 */ 576 cleanUrl(url) { 577 try { 578 url = url.split("?")[0].split("#")[0]; 579 return new URL(url, window.location.href).href; 580 } catch (e) { 581 return url; 582 } 583 } 584 /** 585 * Fetches external stylesheet links from known font providers, retrieves their CSS, 586 * parses them into in-memory CSSStyleSheet objects, and extracts font-family/font-face 587 * information into a structured object. 588 * 589 * @async 590 * @function externalStylesheetsDoc 591 * @returns {Promise<{styleSheets: CSSStyleSheet[], fontPairs: Object}>} An object containing: 592 * - styleSheets: Array of parsed CSSStyleSheet objects (not attached to the DOM). 593 * - fontPairs: An object mapping font URLs to arrays of font variation objects 594 * ({family, weight, style}). 595 * 596 * @example 597 * const { styleSheets, fontPairs } = await externalStylesheetsDoc(); 598 * this.logger.logMessage(fontPairs); 599 */ 600 async externalStylesheetsDoc() { 601 function generateFontPairsFromStyleSheets(styleSheetsArray) { 602 const fontPairs = {}; 603 function _extractFirstUrlFromSrc(srcValue) { 604 if (!srcValue) return null; 605 const urlMatch = srcValue.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/); 606 return urlMatch ? urlMatch[2] : null; 607 } 608 function _cleanFontFamilyName(fontFamilyValue) { 609 if (!fontFamilyValue) return ""; 610 return fontFamilyValue.replace(/^['"]+|['"]+$/g, "").trim(); 611 } 612 if (!styleSheetsArray || !Array.isArray(styleSheetsArray)) { 613 console.warn( 614 "generateFontPairsFromStyleSheets: Input is not a valid array. Received:", 615 styleSheetsArray 616 ); 617 return fontPairs; 618 } 619 if (styleSheetsArray.length === 0) { 620 return fontPairs; 621 } 622 styleSheetsArray.forEach((sheet) => { 623 if (sheet && sheet.cssRules) { 624 try { 625 for (const rule of sheet.cssRules) { 626 if (rule.type === CSSRule.FONT_FACE_RULE) { 627 const cssFontFaceRule = rule; 628 const fontFamily = _cleanFontFamilyName( 629 cssFontFaceRule.style.getPropertyValue("font-family") 630 ); 631 const fontWeight = cssFontFaceRule.style.getPropertyValue("font-weight") || "normal"; 632 const fontStyle = cssFontFaceRule.style.getPropertyValue("font-style") || "normal"; 633 const src = cssFontFaceRule.style.getPropertyValue("src"); 634 const fontUrl = _extractFirstUrlFromSrc(src); 635 if (fontFamily && fontUrl) { 636 const variation = { 637 family: fontFamily, 638 weight: fontWeight, 639 style: fontStyle 640 }; 641 if (!fontPairs[fontUrl]) fontPairs[fontUrl] = []; 642 const variationExists = fontPairs[fontUrl].some( 643 (v) => v.family === variation.family && v.weight === variation.weight && v.style === variation.style 644 ); 645 if (!variationExists) fontPairs[fontUrl].push(variation); 646 } 647 } 648 } 649 } catch (e) { 650 console.warn( 651 "Error processing CSS rules from a stylesheet:", 652 e, 653 sheet 654 ); 655 } 656 } else if (sheet && !sheet.cssRules) { 657 console.warn( 658 "Skipping a stylesheet as its cssRules are not accessible or it is empty:", 659 sheet 660 ); 661 } 662 }); 663 return fontPairs; 664 } 665 const links = [ 666 ...document.querySelectorAll('link[rel="stylesheet"]') 667 ].filter((link) => { 668 try { 669 const linkUrl = new URL(link.href); 670 const currentUrl = new URL(window.location.href); 671 if (linkUrl.origin === currentUrl.origin) { 672 return false;
673 } 674 return !this.isUrlExcludedFromExternalProcessing(link.href); 675 } catch (e) { 676 return false; 677 } 678 }); 679 if (links.length === 0) { 680 this.logger.logMessage("No external CSS links found to process."); 681 return { 682 // Consistent return structure 683 styleSheets: [], 684 // The retrievable CSSStyleSheet objects 685 fontPairs: {} 686 // Processed data from these sheets 687 }; 688 } 689 const fetchedCssPromises = links.map( 690 (linkElement) => fetch(linkElement.href, { mode: "cors" }).then((response) => { 691 if (response.ok) { 692 return response.text(); 693 } 694 console.warn( 695 `Failed to fetch external CSS from ${linkElement.href}: ${response.status} ${response.statusText}` 696 ); 697 return null; 698 }).catch((error) => { 699 console.error( 700 `Network error fetching external CSS from ${linkElement.href}:`, 701 error 702 ); 703 return null; 704 }) 705 ); 706 const cssTexts = await Promise.all(fetchedCssPromises); 707 const temporaryStyleSheets = []; 708 cssTexts.forEach((txt) => { 709 if (txt && txt.trim() !== "") { 710 try { 711 const sheet = new CSSStyleSheet(); 712 sheet.replaceSync(txt); 713 temporaryStyleSheets.push(sheet); 714 } catch (error) { 715 console.error( 716 "Could not parse fetched CSS into a stylesheet:", 717 error, 718 ` 719CSS (first 200 chars): ${txt.substring(0, 200)}...` 720 ); 721 } 722 } 723 }); 724 if (temporaryStyleSheets.length > 0) { 725 this.logger.logMessage( 726 `[Beacon] ${temporaryStyleSheets.length} stylesheet(s) fetched and parsed into CSSStyleSheet objects.` 727 ); 728 } else { 729 this.logger.logMessage( 730 "[Beacon] No stylesheets were successfully parsed from the fetched CSS." 731 ); 732 } 733 const processedFontPairs = generateFontPairsFromStyleSheets(temporaryStyleSheets); 734 return { 735 styleSheets: temporaryStyleSheets, 736 fontPairs: processedFontPairs 737 }; 738 } 739 /** 740 * Asynchronously initializes and parses external font stylesheets. 741 * 742 * Fetches external font stylesheets and font pairs using `externalStylesheetsDoc`, 743 * then stores the parsed results in `externalParsedSheets` and `externalParsedPairs`. 744 * Logs the process and handles errors by resetting `externalParsedSheets` to an empty array. 745 * 746 * @async 747 * @returns {Promise<void>} Resolves when external font stylesheets have been initialized. 748 */ 749 async _initializeExternalFontSheets() { 750 this.logger.logMessage("Initializing external font stylesheets..."); 751 try { 752 const result = await this.externalStylesheetsDoc(); 753 this.externalParsedSheets = result.styleSheets || []; 754 this.externalParsedPairs = result.fontPairs || []; 755 this.logger.logMessage( 756 `Successfully parsed ${this.externalParsedSheets.length} external font stylesheets.` 757 ); 758 } catch (error) { 759 this.logger.logMessage( 760 "Error initializing external font stylesheets:", 761 error 762 ); 763 this.externalParsedSheets = []; 764 } 765 } 766 /** 767 * Retrieves a map of network-loaded fonts. 768 * 769 * This method uses the Performance API to get all resource entries, filters out 770 * the ones that match the font file regex, and maps them to their cleaned URLs. 771 * 772 * @returns {Map} A map where each key is a cleaned URL of a font file and 773 * each value is the original URL of the font file. 774 */ 775 getNetworkLoadedFonts() { 776 return new Map( 777 window.performance.getEntriesByType("resource").filter((resource) => this.FONT_FILE_REGEX.test(resource.name)).map((resource) => [this.cleanUrl(resource.name), resource.name]) 778 ); 779 } 780 /** 781 * Retrieves font-face rules from stylesheets. 782 * 783 * This method scans all stylesheets loaded on the page and collects 784 * font-face rules, including their source URLs, font families, weights,
785 * and styles. It returns an object containing the collected font data. 786 * 787 * @returns {Promise<Object>} An object mapping font families to their respective 788 * URLs and variations. 789 */ 790 async getFontFaceRules() { 791 const stylesheetFonts = {}; 792 const processedUrls = /* @__PURE__ */ new Set(); 793 const processFontFaceRule = (rule, baseHref = null) => { 794 const src = rule.style.getPropertyValue("src"); 795 const fontFamily = rule.style.getPropertyValue("font-family").replace(/['"]/g, "").trim(); 796 const weight = rule.style.getPropertyValue("font-weight") || "400"; 797 const style = rule.style.getPropertyValue("font-style") || "normal"; 798 if (!stylesheetFonts[fontFamily]) { 799 stylesheetFonts[fontFamily] = { urls: [], variations: /* @__PURE__ */ new Set() }; 800 } 801 const extractFirstUrlFromSrc = (srcValue) => { 802 if (!srcValue) return null; 803 const urlMatch = srcValue.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/); 804 return urlMatch ? urlMatch[2] : null; 805 }; 806 const firstUrl = extractFirstUrlFromSrc(src); 807 if (firstUrl) { 808 let rawUrl = firstUrl; 809 if (baseHref) { 810 rawUrl = new URL(rawUrl, baseHref).href; 811 } 812 const normalized = this.cleanUrl(rawUrl); 813 if (!stylesheetFonts[fontFamily].urls.includes(normalized)) { 814 stylesheetFonts[fontFamily].urls.push(normalized); 815 stylesheetFonts[fontFamily].variations.add( 816 JSON.stringify({ weight, style }) 817 ); 818 } 819 } 820 }; 821 const processImportRule = async (rule) => { 822 try { 823 const importUrl = rule.href; 824 if (this.isUrlExcludedFromExternalProcessing(importUrl)) { 825 return; 826 } 827 if (processedUrls.has(importUrl)) { 828 return; 829 } 830 processedUrls.add(importUrl); 831 const response = await fetch(importUrl, { mode: "cors" }); 832 if (!response.ok) { 833 this.logger.logMessage(`Failed to fetch @import CSS: ${response.status}`); 834 return; 835 } 836 const cssText = await response.text(); 837 const tempSheet = new CSSStyleSheet(); 838 tempSheet.replaceSync(cssText); 839 Array.from(tempSheet.cssRules || []).forEach((importedRule) => { 840 if (importedRule instanceof CSSFontFaceRule) { 841 processFontFaceRule(importedRule, importUrl); 842 } 843 }); 844 } catch (error) { 845 this.logger.logMessage(`Error processing @import rule: ${error.message}`); 846 } 847 }; 848 const processSheet = async (sheet) => { 849 try { 850 const rules = Array.from(sheet.cssRules || []); 851 for (const rule of rules) { 852 if (rule instanceof CSSFontFaceRule) { 853 processFontFaceRule(rule, sheet.href); 854 } else if (rule instanceof CSSImportRule) { 855 if (rule.styleSheet) { 856 await processSheet(rule.styleSheet); 857 } else { 858 await processImportRule(rule); 859 } 860 } else if (rule.styleSheet) { 861 await processSheet(rule.styleSheet); 862 } 863 } 864 } catch (e) { 865 if (e.name === "SecurityError" && sheet.href) { 866 if (this.isUrlExcludedFromExternalProcessing(sheet.href)) { 867 return; 868 } 869 if (processedUrls.has(sheet.href)) { 870 return; 871 } 872 processedUrls.add(sheet.href); 873 try { 874 const response = await fetch(sheet.href, { mode: "cors" }); 875 if (response.ok) { 876 const cssText = await response.text(); 877 const tempSheet = new CSSStyleSheet(); 878 tempSheet.replaceSync(cssText); 879 Array.from(tempSheet.cssRules || []).forEach((rule) => { 880 if (rule instanceof CSSFontFaceRule) { 881 processFontFaceRule(rule, sheet.href); 882 } 883 }); 884 const importRegex = /@import\s+url\(['"]?([^'")]+)['"]?\);?/g; 885 let importMatch; 886 while ((importMatch = importRegex.exec(cssText)) !== null) { 887 const importUrl = new URL(importMatch[1], sheet.href).href; 888 if (this.isUrlExcludedFromExternalProcessing(importUrl)) { 889 continue; 890 } 891 if (processedUrls.has(importUrl)) { 892 continue; 893 } 894 processedUrls.add(importUrl); 895 try { 896 const importResponse = await fetch(importUrl, { mode: "cors" }); 897 if (importResponse.ok) { 898 const importCssText = await importResponse.text(); 899 const tempImportSheet = new CSSStyleSheet(); 900 tempImportSheet.replaceSync(importCssText); 901 Array.from(tempImportSheet.cssRules || []).forEac
901h((importedRule) => { 902 if (importedRule instanceof CSSFontFaceRule) { 903 processFontFaceRule(importedRule, importUrl); 904 } 905 }); 906 } 907 } catch (importError) { 908 this.logger.logMessage(`Error fetching @import ${importUrl}: ${importError.message}`); 909 } 910 } 911 } 912 } catch (fetchError) { 913 this.logger.logMessage(`Error fetching stylesheet ${sheet.href}: ${fetchError.message}`); 914 } 915 } else { 916 this.logger.logMessage(`Error processing stylesheet: ${e.message}`); 917 } 918 } 919 }; 920 const sheets = Array.from(document.styleSheets); 921 for (const sheet of sheets) { 922 await processSheet(sheet); 923 } 924 const inlineStyleElements = document.querySelectorAll("style"); 925 for (const styleElement of inlineStyleElements) { 926 const cssText = styleElement.textContent || styleElement.innerHTML || ""; 927 const importRegex = /@import\s+url\s*\(\s*['"]?([^'")]+)['"]?\s*\)\s*;?/g; 928 let importMatch; 929 while ((importMatch = importRegex.exec(cssText)) !== null) { 930 const importUrl = importMatch[1]; 931 if (this.isUrlExcludedFromExternalProcessing(importUrl)) { 932 continue; 933 } 934 if (processedUrls.has(importUrl)) { 935 continue; 936 } 937 processedUrls.add(importUrl); 938 try { 939 const response = await fetch(importUrl, { mode: "cors" }); 940 if (response.ok) { 941 const importCssText = await response.text(); 942 const tempSheet = new CSSStyleSheet(); 943 tempSheet.replaceSync(importCssText); 944 Array.from(tempSheet.cssRules || []).forEach((importedRule) => { 945 if (importedRule instanceof CSSFontFaceRule) { 946 processFontFaceRule(importedRule, importUrl); 947 } 948 }); 949 } 950 } catch (importError) { 951 this.logger.logMessage(`Error fetching inline @import ${importUrl}: ${importError.message}`); 952 } 953 } 954 } 955 Object.values(stylesheetFonts).forEach((fontData) => { 956 fontData.variations = Array.from(fontData.variations).map((v) => JSON.parse(v)); 957 }); 958 return stylesheetFonts; 959 } 960 /** 961 * Checks if an element is above the fold (visible in the viewport without scrolling). 962 * 963 * @param {Element} element - The element to check. 964 * @returns {boolean} True if the element is above the fold, false otherwise. 965 */ 966 isElementAboveFold(element) { 967 if (!this.isElementVisible(element)) return false; 968 const rect = element.getBoundingClientRect(); 969 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 970 const elementTop = rect.top + scrollTop; 971 const foldPosition = window.innerHeight || document.documentElement.clientHeight; 972 return elementTop <= foldPosition; 973 } 974 /** 975 * Checks if an element can be processed for font analysis. 976 * 977 * This method combines checks for whether an element can be styled with font-family 978 * and whether it is above the fold, providing a single method to determine if an 979 * element should be processed during font analysis. 980 * 981 * @param {Element} element - The element to check. 982 * @returns {boolean} True if the element can be processed, false otherwise. 983 */ 984 canElementBeProcessed(element) { 985 return this.canElementBeStyledWithFontFamily(element) && this.isElementAboveFold(element); 986 } 987 /** 988 * Initiates the process of analyzing and summarizing font usage on the page. 989 * This method fetches network-loaded fonts, stylesheet fonts, and external font pairs. 990 * It then processes each element on the page to determine which fonts are used above the fold. 991 * The results are summarized and logged. 992 * 993 * @returns {Promise<void>} A promise that resolves when the analysis is complete. 994 */ 995 async run() { 996 await document.fonts.ready;
997 await this._initializeExternalFontSheets(); 998 const networkLoadedFonts = this.getNetworkLoadedFonts(); 999 const stylesheetFonts = await this.getFontFaceRules(); 1000 const hostedFonts = /* @__PURE__ */ new Map(); 1001 const externalFontsResults = await this.processExternalFonts(this.externalParsedPairs); 1002 const elements = Array.from(document.getElementsByTagName("*")).filter((el) => this.canElementBeProcessed(el)); 1003 elements.forEach((element) => { 1004 const processElementFont = (style, pseudoElement = null) => { 1005 if (!style || !this.isElementVisible(element)) return; 1006 const fontFamily = style.fontFamily.split(",")[0].replace(/['"]+/g, "").trim(); 1007 const hasContent = pseudoElement ? style.content !== "none" && style.content !== '""' : element.textContent.trim(); 1008 if (hasContent && stylesheetFonts[fontFamily]) { 1009 let urls = stylesheetFonts[fontFamily].urls; 1010 if (!this.isExcluded(fontFamily, urls) && !hostedFonts.has(fontFamily)) { 1011 hostedFonts.set(fontFamily, { 1012 elements: /* @__PURE__ */ new Set(), 1013 urls, 1014 variations: stylesheetFonts[fontFamily].variations 1015 }); 1016 hostedFonts.get(fontFamily).elements.add(element); 1017 } 1018 } 1019 }; 1020 try { 1021 processElementFont(window.getComputedStyle(element)); 1022 ["::before", "::after"].forEach((pseudo) => { 1023 processElementFont(window.getComputedStyle(element, pseudo), pseudo); 1024 }); 1025 } catch (e) { 1026 this.logger.logMessage("Error processing element:", e); 1027 } 1028 }); 1029 const aboveTheFoldFonts = this.summarizeMatches(externalFontsResults, hostedFonts, networkLoadedFonts); 1030 if (!Object.keys(aboveTheFoldFonts.allFonts).length && !Object.keys(aboveTheFoldFonts.externalFonts).length && !Object.keys(aboveTheFoldFonts.hostedFonts).length) { 1031 this.logger.logMessage("No fonts found above the fold."); 1032 return; 1033 } 1034 this.logger.logMessage("Above the fold fonts:", aboveTheFoldFonts); 1035 this.aboveTheFoldFonts = [...new Set(Object.values(aboveTheFoldFonts.allFonts).flatMap((font) => font.variations.map((variation) => variation.url)))]; 1036 } 1037 /** 1038 * Summarizes all font matches found on the page 1039 * Creates a comprehensive object containing font usage data 1040 * 1041 * @param {Object} externalFontsResults - Results from External Fonts analysis 1042 * @param {Map} hostedFonts - Map of hosted (non-External) fonts found 1043 * @param {Map} networkLoadedFonts - Map of all font files loaded via network 1044 * @returns {Object} Complete analysis of font usage including locations and counts 1045 */ 1046 summarizeMatches(externalFontsResults, hostedFonts, networkLoadedFonts) { 1047 const allFonts = {}; 1048 const hostedFontsResults = {}; 1049 if (hostedFonts.size > 0) {
1050 hostedFonts.forEach((data, fontFamily) => { 1051 if (data.variations) { 1052 const elements = Array.from(data.elements); 1053 const aboveElements = elements.filter((el) => this.isElementAboveFold(el)); 1054 const belowElements = elements.filter((el) => !this.isElementAboveFold(el)); 1055 data.variations.forEach((variation) => { 1056 let matchingUrl = null; 1057 for (const styleUrl of data.urls) { 1058 const normalizedStyleUrl = this.cleanUrl(styleUrl); 1059 if (networkLoadedFonts.has(normalizedStyleUrl)) { 1060 matchingUrl = networkLoadedFonts.get(normalizedStyleUrl); 1061 break; 1062 } 1063 } 1064 if (matchingUrl) { 1065 if (!allFonts[fontFamily]) { 1066 allFonts[fontFamily] = { 1067 type: "hosted", 1068 variations: [], 1069 elementCount: { 1070 aboveFold: aboveElements.length, 1071 belowFold: belowElements.length, 1072 total: elements.length 1073 }, 1074 urlCount: { 1075 aboveFold: /* @__PURE__ */ new Set(), 1076 belowFold: /* @__PURE__ */ new Set() 1077 } 1078 }; 1079 } 1080 allFonts[fontFamily].variations.push({ 1081 weight: variation.weight, 1082 style: variation.style, 1083 url: matchingUrl, 1084 elementCount: { 1085 aboveFold: aboveElements.length, 1086 belowFold: belowElements.length, 1087 total: elements.length 1088 } 1089 }); 1090 if (aboveElements.length > 0) { 1091 allFonts[fontFamily].urlCount.aboveFold.add(matchingUrl); 1092 } 1093 if (belowElements.length > 0) { 1094 allFonts[fontFamily].urlCount.belowFold.add(matchingUrl); 1095 } 1096 } 1097 }); 1098 if (allFonts[fontFamily]) { 1099 hostedFontsResults[fontFamily] = { 1100 variations: allFonts[fontFamily].variations, 1101 elementCount: { ...allFonts[fontFamily].elementCount }, 1102 urlCount: { ...allFonts[fontFamily].urlCount } 1103 }; 1104 } 1105 } 1106 }); 1107 } 1108 if (Object.keys(externalFontsResults).length > 0) { 1109 Object.entries(externalFontsResults).forEach(([url, data]) => { 1110 const aboveElements = Array.from(data.elements).filter((el) => this.isElementAboveFold(el)); 1111 const belowElements = Array.from(data.elements).filter((el) => !this.isElementAboveFold(el)); 1112 if (data.elementCount.aboveFold > 0 || aboveElements.length > 0) { 1113 data.variations.forEach((variation) => { 1114 if (!allFonts[variation.family]) { 1115 allFonts[variation.family] = { 1116 type: "external", 1117 variations: [], 1118 // Track element counts at font family level 1119 elementCount: { 1120 aboveFold: 0, 1121 belowFold: 0, 1122 total: 0 1123 }, 1124 // Track unique URLs used in each fold location 1125 urlCount: { 1126 aboveFold: /* @__PURE__ */ new Set(), 1127 belowFold: /* @__PURE__ */ new Set() 1128 } 1129 }; 1130 } 1131 allFonts[variation.family].variations.push({ 1132 weight: variation.weight, 1133 style: variation.style, 1134 url, 1135 elementCount: { 1136 aboveFold: aboveElements.length, 1137 belowFold: belowElements.length, 1138 total: data.elements.length 1139 } 1140 }); 1141 allFonts[variation.family].elementCount.aboveFold += aboveElements.length; 1142 allFonts[variation.family].elementCount.belowFold += belowElements.length; 1143 allFonts[variation.family].elementCount.total += data.elements.length; 1144 if (aboveElements.length > 0) { 1145 allFonts[variation.family].urlCount.aboveFold.add(url); 1146 } 1147 if (belowElements.length > 0) { 1148 allFonts[variation.family].urlCount.belowFold.add(url); 1149 } 1150 }); 1151 } 1152 }); 1153 }
1154 Object.values(allFonts).forEach((font) => { 1155 font.urlCount = { 1156 aboveFold: font.urlCount.aboveFold.size, 1157 belowFold: font.urlCount.belowFold.size, 1158 total: (/* @__PURE__ */ new Set([...font.urlCount.aboveFold, ...font.urlCount.belowFold])).size 1159 }; 1160 }); 1161 Object.values(hostedFontsResults).forEach((font) => { 1162 if (font.urlCount.aboveFold instanceof Set) { 1163 font.urlCount = { 1164 aboveFold: font.urlCount.aboveFold.size, 1165 belowFold: font.urlCount.belowFold.size, 1166 total: (/* @__PURE__ */ new Set([...font.urlCount.aboveFold, ...font.urlCount.belowFold])).size 1167 }; 1168 } 1169 }); 1170 return { 1171 externalFonts: Object.fromEntries( 1172 Object.entries(externalFontsResults).filter( 1173 (entry) => entry[1].elementCount.aboveFold > 0 1174 ) 1175 ), 1176 hostedFonts: hostedFontsResults, 1177 allFonts 1178 }; 1179 } 1180 /** 1181 * Processes external font pairs to identify their usage on the page. 1182 * 1183 * This method iterates through all elements on the page, checks if they are above the fold, 1184 * and determines the font information for each element. It then matches the font information 1185 * with the provided external font pairs to identify which fonts are used and where. 1186 * 1187 * @param {Object} fontPairs - An object where each key is a URL and the value is an array of font variations. 1188 * @returns {Promise<Object>} A promise that resolves to an object where each key is a URL and the value is an object c
1188ontaining information about the elements using that font. 1189 */ 1190 async processExternalFonts(fontPairs) { 1191 const matches = /* @__PURE__ */ new Map(); 1192 const elements = Array.from(document.getElementsByTagName("*")).filter((el) => this.canElementBeProcessed(el)); 1193 const fontMap = /* @__PURE__ */ new Map(); 1194 Object.entries(fontPairs).forEach(([url, variations]) => { 1195 variations.forEach((variation) => { 1196 const key = `${variation.family}|${variation.weight}|${variation.style}`; 1197 fontMap.set(key, { url, ...variation }); 1198 }); 1199 }); 1200 const getFontInfoForElement = (style) => { 1201 const family = style.fontFamily.split(",")[0].replace(/['"]+/g, "").trim(); 1202 const weight = style.fontWeight; 1203 const fontStyle = style.fontStyle; 1204 const key = `${family}|${weight}|${fontStyle}`; 1205 let fontInfo = fontMap.get(key); 1206 if (!fontInfo && weight !== "400") { 1207 const fallbackKey = `${family}|400|${fontStyle}`; 1208 fontInfo = fontMap.get(fallbackKey); 1209 } 1210 return fontInfo; 1211 }; 1212 elements.forEach((element) => { 1213 if (element.textContent.trim()) { 1214 const style = window.getComputedStyle(element); 1215 const fontInfo = getFontInfoForElement(style); 1216 if (fontInfo) { 1217 if (!this.isExcluded(fontInfo.family, [fontInfo.url]) && !matches.has(fontInfo.url)) { 1218 matches.set(fontInfo.url, { 1219 elements: /* @__PURE__ */ new Set(), 1220 variations: /* @__PURE__ */ new Set() 1221 }); 1222 matches.get(fontInfo.url).elements.add(element); 1223 matches.get(fontInfo.url).variations.add(JSON.stringify({ 1224 family: fontInfo.family, 1225 weight: fontInfo.weight, 1226 style: fontInfo.style 1227 })); 1228 } 1229 } 1230 } 1231 ["::before", "::after"].forEach((pseudo) => { 1232 const pseudoStyle = window.getComputedStyle(element, pseudo); 1233 if (pseudoStyle.content !== "none" && pseudoStyle.content !== '""') { 1234 const fontInfo = getFontInfoForElement(pseudoStyle); 1235 if (fontInfo) { 1236 if (!this.isExcluded(fontInfo.family, [fontInfo.url]) && !matches.has(fontInfo.url)) { 1237 matches.set(fontInfo.url, { 1238 elements: /* @__PURE__ */ new Set(), 1239 variations: /* @__PURE__ */ new Set() 1240 }); 1241 matches.get(fontInfo.url).elements.add(element); 1242 matches.get(fontInfo.url).variations.add(JSON.stringify({ 1243 family: fontInfo.family, 1244 weight: fontInfo.weight, 1245 style: fontInfo.style 1246 })); 1247 } 1248 } 1249 } 1250 }); 1251 }); 1252 return Object.fromEntries( 1253 Array.from(matches.entries()).map(([url, data]) => [ 1254 url, 1255 { 1256 elementCount: { 1257 aboveFold: Array.from(data.elements).filter((el) => this.isElementAboveFold(el)).length, 1258 total: data.elements.size 1259 }, 1260 variations: Array.from(data.variations).map((v) => JSON.parse(v)), 1261 elements: Array.from(data.elements) 1262 } 1263 ]) 1264 ); 1265 } 1266 /** 1267 * Retrieves the results of the font analysis, specifically the fonts used above the fold. 1268 * This method returns an array containing the URLs of the fonts used above the fold. 1269 * 1270 * @returns {Array<string>} An array of URLs of the fonts used above the fold. 1271 */ 1272 getResults() { 1273 return this.aboveTheFoldFonts; 1274 } 1275 }; 1276 var BeaconPreloadFonts_default = BeaconPreloadFonts; 1277 1278
1278// src/BeaconPreconnectExternalDomain.js 1279 var BeaconPreconnectExternalDomain = class { 1280 constructor(config, logger) { 1281 this.logger = logger; 1282 this.result = []; 1283 this.excludedPatterns = config.preconnect_external_domain_exclusions; 1284 this.eligibleElements = config.preconnect_external_domain_elements; 1285 this.matchedItems = /* @__PURE__ */ new Set(); 1286 this.excludedItems = /* @__PURE__ */ new Set(); 1287 } 1288 /** 1289 * Initiates the process of identifying and logging external domains that require preconnection. 1290 * This method queries the document for eligible elements, processes each element to determine 1291 * if it should be preconnected, and logs the results. 1292 */ 1293 async run() { 1294 const elements = document.querySelectorAll( 1295 `${this.eligibleElements.join(", ")}[src], ${this.eligibleElements.join(", ")}[href], ${this.eligibleElements.join(", ")}[rel], ${this.eligibleElements.join(", ")}[type]` 1296 ); 1297 elements.forEach((el) => this.processElement(el)); 1298 this.logger.logMessage({ matchedItems: this.getMatchedItems(), excludedItems: Array.from(this.excludedItems) }); 1299 } 1300 /** 1301 * Processes a single element to determine if it should be preconnected. 1302 * 1303 * This method checks if the element is excluded based on attribute or domain rules. 1304 * If not excluded, it checks if the element's URL is an external domain and adds it to the list of matched items. 1305 * 1306 * @param {Element} el - The element to process. 1307 */ 1308 processElement(el) { 1309 try { 1310 const url = new URL(el.src || el.href || "", location.href); 1311 if (this.isExcluded(el)) { 1312 this.excludedItems.add(this.createExclusionObject(url, el)); 1313 return; 1314 } 1315 if (this.isExternalDomain(url)) { 1316 this.matchedItems.add(`${url.hostname}-${el.tagName.toLowerCase()}`); 1317 this.result = [...new Set(this.result.concat(url.origin))]; 1318 } 1319 } catch (e) { 1320 this.logger.logMessage(e); 1321 } 1322 } 1323 /** 1324 * Checks if an element is excluded based on exclusions patterns. 1325 * 1326 * This method iterates through the excludedPatterns array and checks if any pattern matches any of the element's attribute or values. 1327 * If a match is found, it returns true, indicating the element is excluded. 1328 * 1329 * @param {Element} el - The element to check. 1330 * @returns {boolean} True if the element is excluded by an attribute rule, false otherwise. 1331 */ 1332 isExcluded(el) { 1333 const outerHTML = el.outerHTML.substring(0, el.outerHTML.indexOf(">") + 1); 1334 return this.excludedPatterns.some( 1335 (pattern) => outerHTML.includes(pattern) 1336 ); 1337 } 1338 /** 1339 * Checks if a URL is excluded based on domain rules. 1340 * 1341 * This method iterates through the excludedPatterns array and checks if any pattern matches the URL's hostname. 1342 * If a match is found, it returns true, indicating the URL is excluded. 1343 * 1344 * @param {URL} url - The URL to check. 1345 * @returns {boolean} True if the URL is excluded by a domain rule, false otherwise. 1346 */ 1347 isExcludedByDomain(url) { 1348 return this.excludedPatterns.some( 1349 (pattern) => pattern.type === "domain" && url.hostname.includes(pattern.value) 1350 ); 1351 } 1352 /** 1353 * Checks if a URL is from an external domain. 1354 * 1355 * This method compares the hostname of the given URL with the hostname of the current location. 1356 * If they are not the same, it indicates the URL is from an external domain. 1357 * 1358 * @param {URL} url - The URL to check. 1359 * @returns {boolean} True if the URL is from an external domain, false otherwise. 1360 */ 1361 isExternalDomain(url) { 1362 return url.hostname !== location.hostname && url.hostname; 1363 } 1364 /** 1365 * Creates an exclusion object based on the URL, element. 1366 * 1367 * @param {URL} url - The URL to create the exclusion object for. 1368 * @param {Element} el - The element to create the exclusion object for. 1369 * @returns {Object} An object with the URL's hostname, the element's tag name, and the reason. 1370 */ 1371 createExclusionObject(url, el) { 1372 return { domain: url.hostname, elementType: el.tagName.toLowerCase() }; 1373 } 1374 /**
1375 * Returns an array of matched items, each item split into its domain and element type. 1376 * 1377 * This method iterates through the matchedItems set, splits each item into its domain and element type using the last hyphen as a delimiter, 1378 * and returns an array of these split items. 1379 * 1380 * @returns {Array} An array of arrays, each containing a domain and an element type. 1381 */ 1382 getMatchedItems() { 1383 return Array.from(this.matchedItems).map((item) => { 1384 const lastHyphenIndex = item.lastIndexOf("-"); 1385 return [ 1386 item.substring(0, lastHyphenIndex), 1387 // Domain 1388 item.substring(lastHyphenIndex + 1) 1389 // Element type 1390 ]; 1391 }); 1392 } 1393 /** 1394 * Returns the array of unique domain names that were found to be external. 1395 * 1396 * This method returns the result array, which contains a list of unique domain names that were identified as external during the analysis process. 1397 * 1398 * @returns {Array} An array of unique domain names. 1399 */ 1400 getResults() { 1401 return this.result; 1402 } 1403 }; 1404 var BeaconPreconnectExternalDomain_default = BeaconPreconnectExternalDomain; 1405 1406
1406// src/Logger.js 1407 var Logger = class { 1408 constructor(enabled) { 1409 this.enabled = enabled; 1410 } 1411 logMessage(label, msg = "") { 1412 if (!this.enabled) { 1413 return; 1414 } 1415 if (msg !== "") { 1416 console.log(label, msg); 1417 return; 1418 } 1419 console.log(label); 1420 } 1421 logColoredMessage(msg, color = "green") { 1422 if (!this.enabled) { 1423 return; 1424 } 1425 console.log(`%c${msg}`, `color: ${color};`); 1426 } 1427 }; 1428 var Logger_default = Logger; 1429 1430
1430// src/BeaconManager.js 1431 var BeaconManager = class { 1432 constructor(config) { 1433 this.config = config; 1434 this.lcpBeacon = null; 1435 this.lrcBeacon = null; 1436 this.preloadFontsBeacon = null; 1437 this.preconnectExternalDomainBeacon = null; 1438 this.infiniteLoopId = null; 1439 this.errorCode = ""; 1440 this.logger = new Logger_default(this.config.debug); 1441 } 1442 async init() { 1443 this.scriptTimer = /* @__PURE__ */ new Date(); 1444 if (!await this._isValidPreconditions()) { 1445 this._finalize(); 1446 return; 1447 } 1448 if (Utils_default.isPageScrolled()) { 1449 this.logger.logMessage("Bailing out because the page has been scrolled"); 1450 this._finalize(); 1451 return; 1452 } 1453 this.infiniteLoopId = setTimeout(() => { 1454 this._handleInfiniteLoop(); 1455 }, 1e4); 1456 const isGeneratedBefore = await this._getGeneratedBefore(); 1457 const shouldGenerateLcp = this.config.status.atf && (isGeneratedBefore === false || isGeneratedBefore.lcp === false); 1458 const shouldGeneratelrc = this.config.status.lrc && (isGeneratedBefore === false || isGeneratedBefore.lrc === false); 1459 const shouldGeneratePreloadFonts = this.config.status.preload_fonts && (isGeneratedBefore === false || isGeneratedBefore.preload_fonts === false); 1460 const shouldGeneratePreconnectExternalDomain = this.config.status.preconnect_external_domain && (isGeneratedBefore === false || isGeneratedBefore.preconnect_external_domain === false); 1461 if (shouldGenerateLcp) { 1462 this.lcpBeacon = new BeaconLcp_default(this.config, this.logger); 1463 await this.lcpBeacon.run(); 1464 } else { 1465 this.logger.logMessage("Not running BeaconLcp because data is already available or feature is disabled"); 1466 } 1467 if (shouldGeneratelrc) { 1468 this.lrcBeacon = new BeaconLrc_default(this.config, this.logger); 1469 await this.lrcBeacon.run(); 1470 } else { 1471 this.logger.logMessage("Not running BeaconLrc because data is already available or feature is disabled"); 1472 } 1473 if (shouldGeneratePreloadFonts) { 1474 this.preloadFontsBeacon = new BeaconPreloadFonts_default(this.config, this.logger); 1475 await this.preloadFontsBeacon.run(); 1476 } else { 1477 this.logger.logMessage("Not running BeaconPreloadFonts because data is already available or feature is disabled"); 1478 } 1479 if (shouldGeneratePreconnectExternalDomain) { 1480 this.preconnectExternalDomainBeacon = new BeaconPreconnectExternalDomain_default(this.config, this.logger); 1481 await this.preconnectExternalDomainBeacon.run(); 1482 } else { 1483 this.logger.logMessage("Not running BeaconPreconnectExternalDomain because data is already available or feature is disabled"); 1484 } 1485 if (shouldGenerateLcp || shouldGeneratelrc || shouldGeneratePreloadFonts || shouldGeneratePreconnectExternalDomain) { 1486 this._saveFinalResultIntoDB(); 1487 } else { 1488 this.logger.logMessage("Not saving results into DB as no beacon features ran."); 1489 this._finalize(); 1490 } 1491 } 1492 async _isValidPreconditions() { 1493 const threshold = { 1494 width: this.config.width_threshold, 1495 height: this.config.height_threshold 1496 }; 1497 if (Utils_default.isNotValidScreensize(this.config.is_mobile, threshold)) { 1498 this.logger.logMessage("Bailing out because screen size is not acceptable"); 1499 return false; 1500 } 1501 return true; 1502 } 1503 async _getGeneratedBefore() { 1504 if (!Utils_default.isPageCached()) { 1505 return false; 1506 } 1507 let data_check = new FormData(); 1508 data_check.append("action", "rocket_check_beacon"); 1509 data_check.append("rocket_beacon_nonce", this.config.nonce); 1510 data_check.append("url", this.config.url); 1511 data_check.append("is_mobile", this.config.is_mobile); 1512 const beacon_data_response = await fetch(this.config.ajax_url, { 1513 method: "POST", 1514 credentials: "same-origin", 1515 body: data_check 1516 }).then((data) => data.json()); 1517 return beacon_data_response.data; 1518 } 1519 _saveFinalResultIntoDB() { 1520 const results = { 1521 lcp: this.lcpBeacon ? this.lcpBeacon.getResults() : null,
1522 lrc: this.lrcBeacon ? this.lrcBeacon.getResults() : null, 1523 preload_fonts: this.preloadFontsBeacon ? this.preloadFontsBeacon.getResults() : null, 1524 preconnect_external_domain: this.preconnectExternalDomainBeacon ? this.preconnectExternalDomainBeacon.getResults() : null 1525 }; 1526 const data = new FormData(); 1527 data.append("action", "rocket_beacon"); 1528 data.append("rocket_beacon_nonce", this.config.nonce); 1529 data.append("url", this.config.url); 1530 data.append("is_mobile", this.config.is_mobile); 1531 data.append("status", this._getFinalStatus()); 1532 data.append("results", JSON.stringify(results)); 1533 fetch(this.config.ajax_url, { 1534 method: "POST", 1535 credentials: "same-origin", 1536 body: data, 1537 headers: { 1538 "wpr-saas-no-intercept": true 1539 } 1540 }).then((response) => response.json()).then((data2) => { 1541 this.logger.logMessage(data2.data.lcp); 1542 }).catch((error) => { 1543 this.logger.logMessage(error); 1544 }).finally(() => { 1545 this._finalize(); 1546 }); 1547 } 1548 _getFinalStatus() { 1549 if ("" !== this.errorCode) { 1550 return this.errorCode; 1551 } 1552 const scriptTime = (/* @__PURE__ */ new Date() - this.scriptTimer) / 1e3; 1553 if (10 <= scriptTime) { 1554 return "timeout"; 1555 } 1556 return "success"; 1557 } 1558 _handleInfiniteLoop() { 1559 this._saveFinalResultIntoDB(); 1560 } 1561 _finalize() { 1562 const beaconscript = document.querySelector('[data-name="wpr-wpr-beacon"]'); 1563 beaconscript.setAttribute("beacon-completed", "true"); 1564 clearTimeout(this.infiniteLoopId); 1565 } 1566 }; 1567 var BeaconManager_default = BeaconManager; 1568 1569
1569// src/BeaconEntryPoint.js 1570 ((rocket_beacon_data) => { 1571 if (!rocket_beacon_data) { 1572 return; 1573 } 1574 const instance = new BeaconManager_default(rocket_beacon_data); 1575 if (document.readyState !== "loading") { 1576 setTimeout(() => { 1577 instance.init(); 1578 }, rocket_beacon_data.delay); 1579 return; 1580 } 1581 document.addEventListener("DOMContentLoaded", () => { 1582 setTimeout(() => { 1583 instance.init(); 1584 }, rocket_beacon_data.delay); 1585 }); 1586 })(window.rocket_beacon_data); 1587 var BeaconEntryPoint_default = BeaconManager_default; 1588})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.