|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +class BeaconPreconnectExternalDomain { |
| 4 | + constructor(config, logger) { |
| 5 | + this.logger = logger; |
| 6 | + this.result = []; |
| 7 | + |
| 8 | + this.excludedPatterns = config.preconnect_external_domain_exclusions; |
| 9 | + this.eligibleElements = config.preconnect_external_domain_elements; |
| 10 | + |
| 11 | + this.matchedItems = new Set(); |
| 12 | + this.excludedItems = new Set(); |
| 13 | + } |
| 14 | + |
| 15 | + /** |
| 16 | + * Initiates the process of identifying and logging external domains that require preconnection. |
| 17 | + * This method queries the document for eligible elements, processes each element to determine |
| 18 | + * if it should be preconnected, and logs the results. |
| 19 | + */ |
| 20 | + async run() { |
| 21 | + const elements = document.querySelectorAll( |
| 22 | + `${this.eligibleElements.join(', ')}[src], ${this.eligibleElements.join(', ')}[href], ${this.eligibleElements.join(', ')}[rel], ${this.eligibleElements.join(', ')}[type]` |
| 23 | + ); |
| 24 | + |
| 25 | + elements.forEach(el => this.processElement(el)); |
| 26 | + |
| 27 | + this.logger.logMessage({matchedItems: this.getMatchedItems(), excludedItems: Array.from(this.excludedItems)}); |
| 28 | + } |
| 29 | + |
| 30 | + /** |
| 31 | + * Processes a single element to determine if it should be preconnected. |
| 32 | + * |
| 33 | + * This method checks if the element is excluded based on attribute or domain rules. |
| 34 | + * If not excluded, it checks if the element's URL is an external domain and adds it to the list of matched items. |
| 35 | + * |
| 36 | + * @param {Element} el - The element to process. |
| 37 | + */ |
| 38 | + processElement(el) { |
| 39 | + try { |
| 40 | + const url = new URL(el.src || el.href || '', location.href); |
| 41 | + |
| 42 | + if (this.isExcluded(el)) { |
| 43 | + this.excludedItems.add(this.createExclusionObject(url, el)); |
| 44 | + return; |
| 45 | + } |
| 46 | + |
| 47 | + if (this.isExternalDomain(url)) { |
| 48 | + this.matchedItems.add(`${url.hostname}-${el.tagName.toLowerCase()}`); |
| 49 | + this.result = [...new Set(this.result.concat(url.origin))]; |
| 50 | + } |
| 51 | + } catch (e) { |
| 52 | + this.logger.logMessage(e); |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * Checks if an element is excluded based on exclusions patterns. |
| 58 | + * |
| 59 | + * This method iterates through the excludedPatterns array and checks if any pattern matches any of the element's attribute or values. |
| 60 | + * If a match is found, it returns true, indicating the element is excluded. |
| 61 | + * |
| 62 | + * @param {Element} el - The element to check. |
| 63 | + * @returns {boolean} True if the element is excluded by an attribute rule, false otherwise. |
| 64 | + */ |
| 65 | + isExcluded(el) { |
| 66 | + const outerHTML = el.outerHTML.substring(0, el.outerHTML.indexOf('>') + 1); |
| 67 | + return this.excludedPatterns.some( |
| 68 | + (pattern) => outerHTML.includes(pattern) |
| 69 | + ); |
| 70 | + } |
| 71 | + |
| 72 | + /** |
| 73 | + * Checks if a URL is excluded based on domain rules. |
| 74 | + * |
| 75 | + * This method iterates through the excludedPatterns array and checks if any pattern matches the URL's hostname. |
| 76 | + * If a match is found, it returns true, indicating the URL is excluded. |
| 77 | + * |
| 78 | + * @param {URL} url - The URL to check. |
| 79 | + * @returns {boolean} True if the URL is excluded by a domain rule, false otherwise. |
| 80 | + */ |
| 81 | + isExcludedByDomain(url) { |
| 82 | + return this.excludedPatterns.some(pattern => |
| 83 | + pattern.type === 'domain' && url.hostname.includes(pattern.value) |
| 84 | + ); |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * Checks if a URL is from an external domain. |
| 89 | + * |
| 90 | + * This method compares the hostname of the given URL with the hostname of the current location. |
| 91 | + * If they are not the same, it indicates the URL is from an external domain. |
| 92 | + * |
| 93 | + * @param {URL} url - The URL to check. |
| 94 | + * @returns {boolean} True if the URL is from an external domain, false otherwise. |
| 95 | + */ |
| 96 | + isExternalDomain(url) { |
| 97 | + return url.hostname !== location.hostname && url.hostname; |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * Creates an exclusion object based on the URL, element. |
| 102 | + * |
| 103 | + * @param {URL} url - The URL to create the exclusion object for. |
| 104 | + * @param {Element} el - The element to create the exclusion object for. |
| 105 | + * @returns {Object} An object with the URL's hostname, the element's tag name, and the reason. |
| 106 | + */ |
| 107 | + createExclusionObject(url, el) { |
| 108 | + return { domain: url.hostname, elementType: el.tagName.toLowerCase()}; |
| 109 | + } |
| 110 | + |
| 111 | + /** |
| 112 | + * Returns an array of matched items, each item split into its domain and element type. |
| 113 | + * |
| 114 | + * This method iterates through the matchedItems set, splits each item into its domain and element type using the last hyphen as a delimiter, |
| 115 | + * and returns an array of these split items. |
| 116 | + * |
| 117 | + * @returns {Array} An array of arrays, each containing a domain and an element type. |
| 118 | + */ |
| 119 | + getMatchedItems() { |
| 120 | + return Array.from(this.matchedItems).map(item => { |
| 121 | + const lastHyphenIndex = item.lastIndexOf('-'); |
| 122 | + return [ |
| 123 | + item.substring(0, lastHyphenIndex), // Domain |
| 124 | + item.substring(lastHyphenIndex + 1) // Element type |
| 125 | + ]; |
| 126 | + }); |
| 127 | + } |
| 128 | + |
| 129 | + /** |
| 130 | + * Returns the array of unique domain names that were found to be external. |
| 131 | + * |
| 132 | + * This method returns the result array, which contains a list of unique domain names that were identified as external during the analysis process. |
| 133 | + * |
| 134 | + * @returns {Array} An array of unique domain names. |
| 135 | + */ |
| 136 | + getResults() { |
| 137 | + return this.result; |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +export default BeaconPreconnectExternalDomain; |
0 commit comments