From 2e9a8e6b46c8719dab25ade9d262d5cffe157c39 Mon Sep 17 00:00:00 2001 From: Peter Hedenskog Date: Sun, 17 May 2026 22:16:30 +0200 Subject: [PATCH] Adopt eslint-plugin-unicorn alongside the rest of the sitespeed.io stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings coach-core onto eslint-plugin-unicorn 64 — the same major sitespeed.io is on — and migrates the eslint config from .cjs to .js (ESM), which is what the plugin requires and what coach-core's already ESM package implies anyway. The unicorn rule set surfaced 354 findings, 276 of them auto-fixable: a sweep through the codebase toward modern array methods (for…of over .forEach, flatMap over map().flat(), some over findIndex truthy checks), node-native helpers (Number.parseInt, String.fromCodePoint, structuredClone, String#slice, String#replaceAll, String.raw, includes), and tidier conditionals (explicit length checks, no nested ternaries, no useless undefined). The remaining hand-fixes are small. The wappalyzer engine moves its priority arrow out of resolve() into a module-level helper; an exec()-as-truthy check becomes a .test(); lib/index.js hoists getFolders to module scope and stops chaining off an awaited stat() call. A single inline disable lives in lib/dom/bestpractice/charset.js because the page's document.characterSet returns the literal 'UTF-8' and the unicorn rule would rewrite it to 'utf8' and silently break detection. The config disables the same six unicorn rules sitespeed.io disables, plus three more that don't fit coach-core's situation: no-null (the vendored wappalyzer engine deliberately uses null to match the upstream data format), import-style (destructured node:* imports are clearer than forcing default imports), and prefer-module scoped to lib/dom/** (those files are not modules — they are IIFE fragments that get concatenated into dist/coach.min.js and evaluated in the browser via WebDriver executeScript). Co-authored-by: Claude noreply@anthropic.com --- eslint.config.cjs | 57 ---- eslint.config.js | 79 +++++ lib/dom/bestpractice/amp.js | 4 +- lib/dom/bestpractice/charset.js | 1 + lib/dom/bestpractice/cumulativeLayoutShift.js | 2 +- lib/dom/bestpractice/imageAltText.js | 2 +- lib/dom/bestpractice/language.js | 2 +- lib/dom/bestpractice/optimizely.js | 4 +- lib/dom/bestpractice/url.js | 6 +- lib/dom/bestpractice/viewport.js | 8 +- lib/dom/info/amp.js | 10 +- lib/dom/info/domDepth.js | 2 +- lib/dom/info/domElements.js | 2 +- lib/dom/info/iframes.js | 2 +- lib/dom/info/localStorageSize.js | 5 +- lib/dom/info/networkConnectionType.js | 8 +- lib/dom/info/scripts.js | 2 +- lib/dom/info/serviceWorker.js | 6 +- lib/dom/info/sessionStorageSize.js | 5 +- lib/dom/info/userTiming.js | 6 +- lib/dom/performance/avoidRenderBlocking.js | 26 +- lib/dom/performance/avoidScalingImages.js | 4 +- lib/dom/performance/cssPrint.js | 2 +- lib/dom/performance/decodingAsync.js | 2 +- lib/dom/performance/firstContentfulPaint.js | 12 +- lib/dom/performance/googleTagManager.js | 2 +- lib/dom/performance/inlineCss.js | 2 +- lib/dom/performance/interactionToNextPaint.js | 8 +- lib/dom/performance/jquery.js | 22 +- lib/dom/performance/largestContentfulPaint.js | 4 +- lib/dom/performance/lazyLoadingImages.js | 5 +- lib/dom/performance/lcpImageHints.js | 4 +- lib/dom/performance/longTasks.js | 2 +- lib/dom/performance/modernImageFormats.js | 10 +- lib/dom/performance/spof.js | 12 +- lib/dom/privacy/facebook.js | 2 +- lib/dom/privacy/fingerprint.js | 2 +- lib/dom/privacy/ga.js | 8 +- lib/dom/privacy/https.js | 2 +- lib/dom/privacy/iframeSandbox.js | 2 +- lib/dom/privacy/referrerPolicy.js | 2 +- lib/dom/privacy/sessionReplay.js | 7 +- lib/dom/privacy/surveillance.js | 7 +- lib/dom/privacy/youtube.js | 2 +- lib/dom/timings/elementTimings.js | 2 +- lib/dom/timings/fullyLoaded.js | 4 +- lib/dom/timings/largestContentfulPaint.js | 4 +- lib/dom/timings/navigationTimings.js | 6 +- lib/dom/timings/paintTimings.js | 6 +- lib/dom/timings/userTimings.js | 14 +- lib/dom/util.js | 50 ++- lib/har/bestpractice/longHeaders.js | 4 +- lib/har/bestpractice/manyHeaders.js | 4 +- lib/har/bestpractice/unnecessaryHeaders.js | 8 +- lib/har/harCutter.js | 8 +- lib/har/index.js | 6 +- lib/har/info/technology.js | 7 +- lib/har/performance/assetsRedirects.js | 8 +- lib/har/performance/cacheHeaders.js | 12 +- lib/har/performance/cacheHeadersLong.js | 14 +- lib/har/performance/compressAssets.js | 8 +- lib/har/performance/connectionKeepAlive.js | 14 +- lib/har/performance/cssSize.js | 12 +- lib/har/performance/documentRedirect.js | 17 +- lib/har/performance/favicon.js | 6 +- lib/har/performance/fewFonts.js | 7 +- lib/har/performance/fewRequestsPerDomain.js | 4 +- lib/har/performance/headerSize.js | 6 +- lib/har/performance/imageSize.js | 6 +- lib/har/performance/javascriptSize.js | 10 +- lib/har/performance/mimeTypes.js | 4 +- lib/har/performance/optimalCssSize.js | 6 +- lib/har/performance/pageSize.js | 8 +- lib/har/performance/privateAssets.js | 18 +- lib/har/performance/responseOk.js | 8 +- .../privacy/contentSecurityPolicyHeader.js | 26 +- .../crossOriginEmbedderPolicyHeader.js | 4 +- .../privacy/crossOriginOpenerPolicyHeader.js | 4 +- .../crossOriginResourcePolicyHeader.js | 4 +- lib/har/privacy/googleReCaptcha.js | 8 +- lib/har/privacy/mixedContent.js | 8 +- lib/har/privacy/nelHeader.js | 4 +- lib/har/privacy/permissionsPolicyHeader.js | 4 +- lib/har/privacy/referrerPolicyHeader.js | 4 +- lib/har/privacy/reportingEndpointsHeader.js | 4 +- .../privacy/strictTransportSecurityHeader.js | 24 +- lib/har/privacy/thirdPartyPrivacy.js | 25 +- lib/har/privacy/xContentTypeOptionsHeader.js | 4 +- lib/har/thirdParty.js | 25 +- lib/har/util.js | 12 +- lib/index.js | 26 +- lib/merge.js | 8 +- lib/technologies/wappalyzer.js | 217 +++++++------ package-lock.json | 298 ++++++++++++++++++ package.json | 1 + 95 files changed, 827 insertions(+), 538 deletions(-) delete mode 100644 eslint.config.cjs create mode 100644 eslint.config.js diff --git a/eslint.config.cjs b/eslint.config.cjs deleted file mode 100644 index d7ecea8..0000000 --- a/eslint.config.cjs +++ /dev/null @@ -1,57 +0,0 @@ -const prettier = require('eslint-plugin-prettier'); -const globals = require('globals'); -const js = require('@eslint/js'); - -module.exports = [ - { - ignores: ['**/dist/', '**/node_modules/', '**/test/', '**/tools/'] - }, - js.configs.recommended, - { - plugins: { - prettier - }, - - languageOptions: { - globals: { - ...globals.node - }, - - ecmaVersion: 'latest', - sourceType: 'module' - }, - - rules: { - 'prettier/prettier': [ - 'error', - { - singleQuote: true, - trailingComma: 'none' - } - ], - - 'no-extra-semi': 0, - 'no-mixed-spaces-and-tabs': 0, - 'no-prototype-builtins': 0 - } - }, - { - files: ['lib/dom/**'], - languageOptions: { - sourceType: 'script', - globals: { - ...globals.browser, - util: true - } - } - }, - { - // util.js itself declares util locally, so don't treat it as a global there. - files: ['lib/dom/util.js'], - languageOptions: { - globals: { - util: 'off' - } - } - } -]; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..f8ea4d2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,79 @@ +import prettier from 'eslint-plugin-prettier'; +import unicorn from 'eslint-plugin-unicorn'; +import globals from 'globals'; +import js from '@eslint/js'; + +export default [ + { + ignores: ['**/dist/', '**/node_modules/', '**/test/', '**/tools/'] + }, + js.configs.recommended, + unicorn.configs.recommended, + { + plugins: { + prettier + }, + + languageOptions: { + globals: { + ...globals.node + }, + + ecmaVersion: 'latest', + sourceType: 'module' + }, + + rules: { + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + trailingComma: 'none' + } + ], + + 'no-extra-semi': 0, + 'no-mixed-spaces-and-tabs': 0, + 'no-prototype-builtins': 0, + 'unicorn/filename-case': 0, + 'unicorn/prevent-abbreviations': 0, + 'unicorn/no-array-reduce': 0, + 'unicorn/no-array-sort': 0, + 'unicorn/no-array-reverse': 0, + 'unicorn/prefer-spread': 0, + // Destructured imports from node:* namespaces are clearer than + // forcing `import path from 'node:path'` everywhere. + 'unicorn/import-style': 0, + // The vendored wappalyzer engine intentionally uses null to match + // the upstream data format. null vs undefined is semantically + // load-bearing in a few places, not a style preference. + 'unicorn/no-null': 0 + } + }, + { + // The DOM advice rules are not modules — they are IIFE script fragments + // that get concatenated into dist/coach.min.js and run inside the + // browser via WebDriver's executeScript. `"use strict"` is intentional + // there; eslint's `prefer-module` rule does not apply. + files: ['lib/dom/**'], + rules: { + 'unicorn/prefer-module': 0 + }, + languageOptions: { + sourceType: 'script', + globals: { + ...globals.browser, + util: true + } + } + }, + { + // util.js itself declares util locally, so don't treat it as a global there. + files: ['lib/dom/util.js'], + languageOptions: { + globals: { + util: 'off' + } + } + } +]; diff --git a/lib/dom/bestpractice/amp.js b/lib/dom/bestpractice/amp.js index a68c8a6..aaeb3ca 100644 --- a/lib/dom/bestpractice/amp.js +++ b/lib/dom/bestpractice/amp.js @@ -2,10 +2,10 @@ 'use strict'; const offending = []; - const html = document.getElementsByTagName('html')[0]; + const html = document.querySelectorAll('html')[0]; let score = 100; - if ((html && html.getAttribute('amp-version')) || window.AMP) { + if ((html && html.getAttribute('amp-version')) || globalThis.AMP) { score = 0; } diff --git a/lib/dom/bestpractice/charset.js b/lib/dom/bestpractice/charset.js index ce45d99..c157839 100644 --- a/lib/dom/bestpractice/charset.js +++ b/lib/dom/bestpractice/charset.js @@ -9,6 +9,7 @@ message = 'The page is missing a character set. If you use Chrome/Firefox we know you are missing it, if you use another browser, it could be an implementation problem.'; score = 0; + // eslint-disable-next-line unicorn/text-encoding-identifier-case } else if (charSet !== 'UTF-8') { message = 'You are not using charset UTF-8?'; score = 50; diff --git a/lib/dom/bestpractice/cumulativeLayoutShift.js b/lib/dom/bestpractice/cumulativeLayoutShift.js index 64b5673..547dbb2 100644 --- a/lib/dom/bestpractice/cumulativeLayoutShift.js +++ b/lib/dom/bestpractice/cumulativeLayoutShift.js @@ -6,7 +6,7 @@ let score = 0; let max = 0; const supported = PerformanceObserver.supportedEntryTypes; - if (!supported || supported.indexOf('layout-shift') === -1) { + if (!supported || !supported.includes('layout-shift')) { advice = 'Layout Shift is not supported in this browser'; } else { // See https://web.dev/layout-instability-api diff --git a/lib/dom/bestpractice/imageAltText.js b/lib/dom/bestpractice/imageAltText.js index 20c2439..f634856 100644 --- a/lib/dom/bestpractice/imageAltText.js +++ b/lib/dom/bestpractice/imageAltText.js @@ -6,7 +6,7 @@ // / role="presentation" / aria-hidden="true" for purely decorative images. // A missing alt attribute entirely is the failure case. const offending = []; - const images = document.getElementsByTagName('img'); + const images = document.querySelectorAll('img'); for (let i = 0, len = images.length; i < len; i++) { const img = images[i]; const hasAlt = img.hasAttribute('alt'); diff --git a/lib/dom/bestpractice/language.js b/lib/dom/bestpractice/language.js index 80e9fcc..4cb8a75 100644 --- a/lib/dom/bestpractice/language.js +++ b/lib/dom/bestpractice/language.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - const html = document.getElementsByTagName('html'); + const html = document.querySelectorAll('html'); const language = html[0].getAttribute('lang'); let score = 100; let message = ''; diff --git a/lib/dom/bestpractice/optimizely.js b/lib/dom/bestpractice/optimizely.js index e7ee74a..a1137ca 100644 --- a/lib/dom/bestpractice/optimizely.js +++ b/lib/dom/bestpractice/optimizely.js @@ -6,14 +6,14 @@ let score = 100; let advice = ''; - scripts.forEach(function (script) { + for (const script of scripts) { if (util.getHostname(script) === 'cdn.optimizely.com') { offending.push(script); score = 0; advice = 'The page is using Optimizely. Use it with care because it hurts your performance. Only turn it on (= load the JavaScript) when you run your A/B tests. Then when you are finished make sure to turn it off.'; } - }); + } return { id: 'optimizely', diff --git a/lib/dom/bestpractice/url.js b/lib/dom/bestpractice/url.js index 8fb7587..a81beff 100644 --- a/lib/dom/bestpractice/url.js +++ b/lib/dom/bestpractice/url.js @@ -6,7 +6,7 @@ let message = ''; // ok all Java lovers, please do not use the sessionid in your URLs - if (url.indexOf('?') > -1 && url.indexOf('jsessionid') > url.indexOf('?')) { + if (url.includes('?') && url.indexOf('jsessionid') > url.indexOf('?')) { score = 0; message = 'The page has the session id for the user as a parameter, please change so the session handling is done only with cookies. '; @@ -27,7 +27,7 @@ ' characters long. Try to make it less than 100 characters. '; } - if (url.indexOf(' ') > -1 || url.indexOf('%20') > -1) { + if (url.includes(' ') || url.includes('%20')) { score -= 10; message += 'Could the developer or the CMS be on Windows? Avoid using spaces in the URLs, use hyphens or underscores. '; @@ -39,7 +39,7 @@ description: 'A clean URL is good for the user and for SEO. Make them human readable, avoid too long URLs, spaces in the URL, too many request parameters, and never ever have the session id in your URL.', advice: message, - score: score < 0 ? 0 : score, + score: Math.max(score, 0), weight: 2, severity: 'info', offending: [], diff --git a/lib/dom/bestpractice/viewport.js b/lib/dom/bestpractice/viewport.js index 1a332eb..4535b7e 100644 --- a/lib/dom/bestpractice/viewport.js +++ b/lib/dom/bestpractice/viewport.js @@ -7,7 +7,7 @@ // user-scalable=no or maximum-scale=1 also breaks pinch-to-zoom and is an // accessibility regression, so we flag those values too. let content = ''; - const metas = document.getElementsByTagName('meta'); + const metas = document.querySelectorAll('meta'); for (let i = 0, len = metas.length; i < len; i++) { const name = metas[i].getAttribute('name'); if (name && name.toLowerCase() === 'viewport') { @@ -25,14 +25,14 @@ advice = 'The page is missing a viewport meta tag. Add so the browser lays the page out at the device width.'; } else { - if (lower.indexOf('width=device-width') === -1) { + if (!lower.includes('width=device-width')) { score -= 50; advice += 'The viewport meta tag does not contain width=device-width, the browser may use a desktop-width fallback. '; } if ( - lower.indexOf('user-scalable=no') > -1 || - lower.indexOf('user-scalable=0') > -1 + lower.includes('user-scalable=no') || + lower.includes('user-scalable=0') ) { score -= 30; advice += diff --git a/lib/dom/info/amp.js b/lib/dom/info/amp.js index 2478cbf..d8637ff 100644 --- a/lib/dom/info/amp.js +++ b/lib/dom/info/amp.js @@ -1,11 +1,9 @@ (function () { 'use strict'; - const html = document.getElementsByTagName('html')[0]; + const html = document.querySelectorAll('html')[0]; - if ((html && html.getAttribute('amp-version')) || window.AMP) { - return html.getAttribute('amp-version') || true; - } else { - return false; - } + return (html && html.getAttribute('amp-version')) || globalThis.AMP + ? html.getAttribute('amp-version') || true + : false; })(); diff --git a/lib/dom/info/domDepth.js b/lib/dom/info/domDepth.js index e96f26f..6e21f8d 100644 --- a/lib/dom/info/domDepth.js +++ b/lib/dom/info/domDepth.js @@ -2,7 +2,7 @@ 'use strict'; function domDepth(document) { - const allElems = document.getElementsByTagName('*'); + const allElems = document.querySelectorAll('*'); let allElemsLen = allElems.length; let totalParents = 0; let maxParents = 0; diff --git a/lib/dom/info/domElements.js b/lib/dom/info/domElements.js index 463c064..463adda 100644 --- a/lib/dom/info/domElements.js +++ b/lib/dom/info/domElements.js @@ -1,5 +1,5 @@ (function () { 'use strict'; - return document.getElementsByTagName('*').length; + return document.querySelectorAll('*').length; })(); diff --git a/lib/dom/info/iframes.js b/lib/dom/info/iframes.js index d262824..f788f15 100644 --- a/lib/dom/info/iframes.js +++ b/lib/dom/info/iframes.js @@ -1,5 +1,5 @@ (function () { 'use strict'; - return document.getElementsByTagName('iframe').length; + return document.querySelectorAll('iframe').length; })(); diff --git a/lib/dom/info/localStorageSize.js b/lib/dom/info/localStorageSize.js index 03886f4..1d5dd39 100644 --- a/lib/dom/info/localStorageSize.js +++ b/lib/dom/info/localStorageSize.js @@ -18,9 +18,8 @@ } } try { - return storageSize(window.localStorage); - // eslint-disable-next-line no-unused-vars - } catch (e) { + return storageSize(globalThis.localStorage); + } catch { return 'Could not access localStorage.'; } })(); diff --git a/lib/dom/info/networkConnectionType.js b/lib/dom/info/networkConnectionType.js index 5abc45e..bb5e4a4 100644 --- a/lib/dom/info/networkConnectionType.js +++ b/lib/dom/info/networkConnectionType.js @@ -1,9 +1,7 @@ (function () { 'use strict'; - if (window.navigator.connection) { - return window.navigator.connection.effectiveType; - } else { - return 'unknown'; - } + return globalThis.navigator.connection + ? globalThis.navigator.connection.effectiveType + : 'unknown'; })(); diff --git a/lib/dom/info/scripts.js b/lib/dom/info/scripts.js index 8902c81..873a115 100644 --- a/lib/dom/info/scripts.js +++ b/lib/dom/info/scripts.js @@ -1,5 +1,5 @@ (function () { 'use strict'; - return document.getElementsByTagName('script').length; + return document.querySelectorAll('script').length; })(); diff --git a/lib/dom/info/serviceWorker.js b/lib/dom/info/serviceWorker.js index 1d74de4..6def768 100644 --- a/lib/dom/info/serviceWorker.js +++ b/lib/dom/info/serviceWorker.js @@ -4,9 +4,9 @@ if ('serviceWorker' in navigator) { // Only report activated service workers if (navigator.serviceWorker.controller) { - if (navigator.serviceWorker.controller.state === 'activated') { - return navigator.serviceWorker.controller.scriptURL; - } else return false; + return navigator.serviceWorker.controller.state === 'activated' + ? navigator.serviceWorker.controller.scriptURL + : false; } else { return false; } diff --git a/lib/dom/info/sessionStorageSize.js b/lib/dom/info/sessionStorageSize.js index 0821623..71b2ac1 100644 --- a/lib/dom/info/sessionStorageSize.js +++ b/lib/dom/info/sessionStorageSize.js @@ -14,9 +14,8 @@ } try { - return storageSize(window.sessionStorage); - // eslint-disable-next-line no-unused-vars - } catch (e) { + return storageSize(globalThis.sessionStorage); + } catch { return 'Could not access sessionStorage'; } })(); diff --git a/lib/dom/info/userTiming.js b/lib/dom/info/userTiming.js index 6d844d4..8749c9c 100644 --- a/lib/dom/info/userTiming.js +++ b/lib/dom/info/userTiming.js @@ -3,9 +3,9 @@ let mark = 0; let measure = 0; - if (window.performance && window.performance.getEntriesByType) { - measure = window.performance.getEntriesByType('measure').length; - mark = window.performance.getEntriesByType('mark').length; + if (globalThis.performance && globalThis.performance.getEntriesByType) { + measure = globalThis.performance.getEntriesByType('measure').length; + mark = globalThis.performance.getEntriesByType('mark').length; } return { marks: mark, diff --git a/lib/dom/performance/avoidRenderBlocking.js b/lib/dom/performance/avoidRenderBlocking.js index 418e4fb..3a40099 100644 --- a/lib/dom/performance/avoidRenderBlocking.js +++ b/lib/dom/performance/avoidRenderBlocking.js @@ -19,7 +19,10 @@ function testByType(assetUrl) { const domain = util.getHostname(assetUrl); // if it is from different domain or not - if (domain !== docDomain) { + if (domain === docDomain) { + offending.push(assetUrl); + score += 5; + } else { offending.push(assetUrl); // is this the first time this domain is used? if (!util.exists(domain, domains)) { @@ -28,9 +31,6 @@ domains.push(domain); } score += 5; - } else { - offending.push(assetUrl); - score += 5; } } @@ -40,8 +40,8 @@ if (styles.length > 0) { message = ''; // check the size - styles.forEach(function (url) { - if (util.getTransferSize(url) > 14500) { + for (const url of styles) { + if (util.getTransferSize(url) > 14_500) { offending.push(url); score += 5; blockingCSS++; @@ -50,14 +50,14 @@ url + ' is larger than the magic number TCP window size 14.5 kB. Make the file smaller and the page will render faster. '; } - }); + } } if (scripts.length > 0) { score += scripts.length * 10; - scripts.forEach(function (url) { + for (const url of scripts) { offending.push(url); blockingJS++; - }); + } message += "Avoid loading synchronously JavaScript inside of head, you shouldn't need JavaScript to render your page! "; } @@ -65,13 +65,13 @@ // Recommendations for HTTP3 to come } else { // we are using HTTP/1 - styles.forEach(function (style) { + for (const style of styles) { testByType(style); - }); + } blockingCSS = styles.length; - scripts.forEach(function (script) { + for (const script of scripts) { testByType(script); - }); + } blockingJS = scripts.length; } diff --git a/lib/dom/performance/avoidScalingImages.js b/lib/dom/performance/avoidScalingImages.js index 129d9e7..488fda1 100644 --- a/lib/dom/performance/avoidScalingImages.js +++ b/lib/dom/performance/avoidScalingImages.js @@ -3,9 +3,7 @@ const minLimit = 100; const offending = []; - const images = Array.prototype.slice.call( - document.getElementsByTagName('img') - ); + const images = Array.prototype.slice.call(document.querySelectorAll('img')); let score = 0; let message = ''; diff --git a/lib/dom/performance/cssPrint.js b/lib/dom/performance/cssPrint.js index 49a94d4..095ed31 100644 --- a/lib/dom/performance/cssPrint.js +++ b/lib/dom/performance/cssPrint.js @@ -2,7 +2,7 @@ 'use strict'; const offending = []; - const links = document.getElementsByTagName('link'); + const links = document.querySelectorAll('link'); for (let i = 0, len = links.length; i < len; i++) { if (links[i].media === 'print') { diff --git a/lib/dom/performance/decodingAsync.js b/lib/dom/performance/decodingAsync.js index 5eaf21a..d464e3d 100644 --- a/lib/dom/performance/decodingAsync.js +++ b/lib/dom/performance/decodingAsync.js @@ -11,7 +11,7 @@ // first-paint image where they want to ensure synchronous decode) and // we don't flag `decoding="auto"` (the explicit default). const offending = []; - const images = document.getElementsByTagName('img'); + const images = document.querySelectorAll('img'); let unhinted = 0; let total = 0; diff --git a/lib/dom/performance/firstContentfulPaint.js b/lib/dom/performance/firstContentfulPaint.js index 4c85b2d..119a863 100644 --- a/lib/dom/performance/firstContentfulPaint.js +++ b/lib/dom/performance/firstContentfulPaint.js @@ -21,13 +21,11 @@ fcp )}). It is in the Google Web Vitals poor range, slower than 3 seconds.`; - let t = window.performance.getEntriesByType('navigation')[0]; - if (t) { - if (Number(t.responseStart.toFixed(0)) > 1000) { - advice += `The page has a high time to first byte (TTFB) ${util.ms( - t.responseStart - )} that you should look into to improve first contentful paint.`; - } + let t = globalThis.performance.getEntriesByType('navigation')[0]; + if (t && Number(t.responseStart.toFixed(0)) > 1000) { + advice += `The page has a high time to first byte (TTFB) ${util.ms( + t.responseStart + )} that you should look into to improve first contentful paint.`; } } } else { diff --git a/lib/dom/performance/googleTagManager.js b/lib/dom/performance/googleTagManager.js index 04e9dfd..a691ee4 100644 --- a/lib/dom/performance/googleTagManager.js +++ b/lib/dom/performance/googleTagManager.js @@ -2,7 +2,7 @@ 'use strict'; var score = 100; - if (window.google_tag_manager) { + if (globalThis.google_tag_manager) { score = 0; } diff --git a/lib/dom/performance/inlineCss.js b/lib/dom/performance/inlineCss.js index a16f113..4e81d26 100644 --- a/lib/dom/performance/inlineCss.js +++ b/lib/dom/performance/inlineCss.js @@ -4,7 +4,7 @@ const offending = []; const cssFilesInHead = util.getCSSFiles(document.head); const styles = Array.prototype.slice.call( - window.document.head.getElementsByTagName('style') + globalThis.document.head.querySelectorAll('style') ); let message = ''; let score = 0; diff --git a/lib/dom/performance/interactionToNextPaint.js b/lib/dom/performance/interactionToNextPaint.js index 609d336..f88c2c4 100644 --- a/lib/dom/performance/interactionToNextPaint.js +++ b/lib/dom/performance/interactionToNextPaint.js @@ -20,7 +20,7 @@ const needImprovement = 500; const supported = PerformanceObserver.supportedEntryTypes; - if (!supported || supported.indexOf('event') === -1) { + if (!supported || !supported.includes('event')) { advice = 'Interaction to Next Paint is not supported in this browser.'; } else { const observer = new PerformanceObserver(() => {}); @@ -32,8 +32,7 @@ buffered: true, durationThreshold: 40 }); - // eslint-disable-next-line no-unused-vars - } catch (e) { + } catch { observer.observe({ type: 'event', buffered: true }); } const entries = observer.takeRecords(); @@ -48,8 +47,7 @@ // group. interactionId === 0 means the event isn't part of an // interaction (synthetic, scrolling, hover etc.); we ignore those. const byInteraction = {}; - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; + for (const entry of entries) { const id = entry.interactionId; if (!id) continue; if (!byInteraction[id] || entry.duration > byInteraction[id].duration) { diff --git a/lib/dom/performance/jquery.js b/lib/dom/performance/jquery.js index f0ef06b..65e6c5b 100644 --- a/lib/dom/performance/jquery.js +++ b/lib/dom/performance/jquery.js @@ -3,22 +3,26 @@ const versions = []; // check that we got a jQuery - if (typeof window.jQuery === 'function') { + if (typeof globalThis.jQuery === 'function') { // https://github.com/sitespeedio/coach-core/issues/21#issuecomment-717041439 - let keepRef = window.$; - versions.push(window.jQuery.fn.jquery); - let old = window.jQuery; + let keepRef = globalThis.$; + versions.push(globalThis.jQuery.fn.jquery); + let old = globalThis.jQuery; while (old.fn && old.fn.jquery) { - old = window.jQuery.noConflict(true); - if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.jquery) { + old = globalThis.jQuery.noConflict(true); + if ( + !globalThis.jQuery || + !globalThis.jQuery.fn || + !globalThis.jQuery.fn.jquery + ) { break; } - if (old.fn.jquery === window.jQuery.fn.jquery) { + if (old.fn.jquery === globalThis.jQuery.fn.jquery) { break; } - versions.push(window.jQuery.fn.jquery); + versions.push(globalThis.jQuery.fn.jquery); } - window.jQuery = window.$ = keepRef; + globalThis.jQuery = globalThis.$ = keepRef; } // TODO also add check for jQuery version. If we have a really old version (1 year old?) then show it! diff --git a/lib/dom/performance/largestContentfulPaint.js b/lib/dom/performance/largestContentfulPaint.js index 50bdb01..8d5e6f0 100644 --- a/lib/dom/performance/largestContentfulPaint.js +++ b/lib/dom/performance/largestContentfulPaint.js @@ -4,14 +4,14 @@ let advice = ''; const offending = []; const supported = PerformanceObserver.supportedEntryTypes; - if (!supported || supported.indexOf('largest-contentful-paint') === -1) { + if (!supported || !supported.includes('largest-contentful-paint')) { advice = 'Largest contentful paint is not supported in this browser'; } else { const observer = new PerformanceObserver(() => {}); observer.observe({ type: 'largest-contentful-paint', buffered: true }); const entries = observer.takeRecords(); if (entries.length > 0) { - const largestEntry = entries[entries.length - 1]; + const largestEntry = entries.at(-1); const good = 2500; const needImprovement = 4000; diff --git a/lib/dom/performance/lazyLoadingImages.js b/lib/dom/performance/lazyLoadingImages.js index bcd7ea9..7278554 100644 --- a/lib/dom/performance/lazyLoadingImages.js +++ b/lib/dom/performance/lazyLoadingImages.js @@ -17,7 +17,7 @@ const offending = []; let belowFoldCount = 0; - const images = document.getElementsByTagName('img'); + const images = document.querySelectorAll('img'); for (let i = 0, len = images.length; i < len; i++) { const img = images[i]; @@ -27,8 +27,7 @@ let rect; try { rect = img.getBoundingClientRect(); - // eslint-disable-next-line no-unused-vars - } catch (e) { + } catch { continue; } // Skip non-rendered images (display:none etc.) — they have zero diff --git a/lib/dom/performance/lcpImageHints.js b/lib/dom/performance/lcpImageHints.js index 26faa19..688a6b8 100644 --- a/lib/dom/performance/lcpImageHints.js +++ b/lib/dom/performance/lcpImageHints.js @@ -20,7 +20,7 @@ const offending = []; const supported = PerformanceObserver.supportedEntryTypes; - if (!supported || supported.indexOf('largest-contentful-paint') === -1) { + if (!supported || !supported.includes('largest-contentful-paint')) { advice = 'Largest Contentful Paint is not supported in this browser, so the LCP image hints cannot be assessed.'; } else { @@ -30,7 +30,7 @@ if (entries.length === 0) { advice = 'No LCP entries were observed during this snapshot.'; } else { - const entry = entries[entries.length - 1]; + const entry = entries.at(-1); const el = entry.element; if (!el || el.tagName !== 'IMG') { // The LCP element is text or some other non-image. Nothing to diff --git a/lib/dom/performance/longTasks.js b/lib/dom/performance/longTasks.js index 89dcf6a..c441a93 100644 --- a/lib/dom/performance/longTasks.js +++ b/lib/dom/performance/longTasks.js @@ -10,7 +10,7 @@ let advice = 'The page do not have any CPU Long Tasks.'; const supported = PerformanceObserver.supportedEntryTypes; - if (!supported || supported.indexOf('longtask') === -1) { + if (!supported || !supported.includes('longtask')) { advice = 'The Long Task API is not supported in this browser.'; } else { const longTaskObserver = new PerformanceObserver(() => {}); diff --git a/lib/dom/performance/modernImageFormats.js b/lib/dom/performance/modernImageFormats.js index 62e6e32..44f4383 100644 --- a/lib/dom/performance/modernImageFormats.js +++ b/lib/dom/performance/modernImageFormats.js @@ -19,9 +19,9 @@ if (!parent || parent.tagName !== 'PICTURE') { return false; } - const sources = parent.getElementsByTagName('source'); - for (let i = 0; i < sources.length; i++) { - const type = (sources[i].getAttribute('type') || '').toLowerCase(); + const sources = parent.querySelectorAll('source'); + for (const source of sources) { + const type = (source.getAttribute('type') || '').toLowerCase(); if ( type === 'image/avif' || type === 'image/webp' || @@ -29,7 +29,7 @@ ) { return true; } - const srcset = sources[i].getAttribute('srcset') || ''; + const srcset = source.getAttribute('srcset') || ''; if (MODERN_EXT_RE.test(srcset)) { return true; } @@ -45,7 +45,7 @@ const offending = []; let legacyOnly = 0; let total = 0; - const images = document.getElementsByTagName('img'); + const images = document.querySelectorAll('img'); for (let i = 0, len = images.length; i < len; i++) { const img = images[i]; const src = img.currentSrc || img.src || ''; diff --git a/lib/dom/performance/spof.js b/lib/dom/performance/spof.js index 05e74c8..a309f05 100644 --- a/lib/dom/performance/spof.js +++ b/lib/dom/performance/spof.js @@ -9,30 +9,30 @@ const styles = util.getCSSFiles(document.head); let score = 0; - styles.forEach(function (style) { + for (const style of styles) { const styleDomain = util.getHostname(style); if (styleDomain !== docDomain) { offending.push(style); - if (offendingDomains.indexOf(styleDomain) === -1) { + if (!offendingDomains.includes(styleDomain)) { offendingDomains.push(styleDomain); score += 10; } } - }); + } // do we have any JS loaded inside of head from a different domain? const scripts = util.getSynchJSFiles(document.head); - scripts.forEach(function (script) { + for (const script of scripts) { const scriptDomain = util.getHostname(script); if (scriptDomain !== docDomain) { offending.push(script); - if (offendingDomains.indexOf(scriptDomain) === -1) { + if (!offendingDomains.includes(scriptDomain)) { offendingDomains.push(scriptDomain); score += 10; } } - }); + } return { id: 'spof', diff --git a/lib/dom/privacy/facebook.js b/lib/dom/privacy/facebook.js index decf11f..54a9468 100644 --- a/lib/dom/privacy/facebook.js +++ b/lib/dom/privacy/facebook.js @@ -4,7 +4,7 @@ const offending = []; let score = 100; - if (window.FB) { + if (globalThis.FB) { score = 0; } diff --git a/lib/dom/privacy/fingerprint.js b/lib/dom/privacy/fingerprint.js index 353f49b..c6fbc6f 100644 --- a/lib/dom/privacy/fingerprint.js +++ b/lib/dom/privacy/fingerprint.js @@ -4,7 +4,7 @@ const offending = []; let score = 100; - if (window.FingerprintJS || window.Fingerprint2) { + if (globalThis.FingerprintJS || globalThis.Fingerprint2) { score = 0; } diff --git a/lib/dom/privacy/ga.js b/lib/dom/privacy/ga.js index b7693d2..03e70b0 100644 --- a/lib/dom/privacy/ga.js +++ b/lib/dom/privacy/ga.js @@ -7,11 +7,11 @@ // Detect classic Google Analytics (analytics.js / window.ga) and // Google Analytics 4 (gtag.js, which exposes window.gtag and a dataLayer // populated with a 'config' command for a G-XXXXXXXXXX measurement id). - const hasClassicGA = !!(window.ga && window.ga.create); + const hasClassicGA = !!(globalThis.ga && globalThis.ga.create); const hasGA4 = - typeof window.gtag === 'function' && - Array.isArray(window.dataLayer) && - window.dataLayer.some(function (entry) { + typeof globalThis.gtag === 'function' && + Array.isArray(globalThis.dataLayer) && + globalThis.dataLayer.some(function (entry) { return ( entry && entry[0] === 'config' && diff --git a/lib/dom/privacy/https.js b/lib/dom/privacy/https.js index c19bbf0..d63dde2 100644 --- a/lib/dom/privacy/https.js +++ b/lib/dom/privacy/https.js @@ -5,7 +5,7 @@ let score = 100; let message = ''; - if (url.indexOf('https://') === -1) { + if (!url.includes('https://')) { score = 0; message = 'What!! The page is not using HTTPS. Every unencrypted HTTP request reveals information about user’s behavior, read more about it at https://https.cio.gov/everything/. You can get a totally free SSL/TLS certificate from https://letsencrypt.org/.'; diff --git a/lib/dom/privacy/iframeSandbox.js b/lib/dom/privacy/iframeSandbox.js index 0d5a7e8..3c779c5 100644 --- a/lib/dom/privacy/iframeSandbox.js +++ b/lib/dom/privacy/iframeSandbox.js @@ -8,7 +8,7 @@ const pageHost = util.getHostname(document.URL).toLowerCase(); const offending = []; - const iframes = document.getElementsByTagName('iframe'); + const iframes = document.querySelectorAll('iframe'); for (let i = 0, len = iframes.length; i < len; i++) { const iframe = iframes[i]; if (!iframe.src) { diff --git a/lib/dom/privacy/referrerPolicy.js b/lib/dom/privacy/referrerPolicy.js index bdda966..d3500ae 100644 --- a/lib/dom/privacy/referrerPolicy.js +++ b/lib/dom/privacy/referrerPolicy.js @@ -6,7 +6,7 @@ // header is preferred and is checked separately by the HAR rule. This DOM // rule is the fallback signal for runs without a HAR. let metaContent = ''; - const metas = document.getElementsByTagName('meta'); + const metas = document.querySelectorAll('meta'); for (let i = 0, len = metas.length; i < len; i++) { const name = metas[i].getAttribute('name'); if (name && name.toLowerCase() === 'referrer') { diff --git a/lib/dom/privacy/sessionReplay.js b/lib/dom/privacy/sessionReplay.js index bb4a4d0..af4ebd5 100644 --- a/lib/dom/privacy/sessionReplay.js +++ b/lib/dom/privacy/sessionReplay.js @@ -56,8 +56,7 @@ if (!host) { return false; } - for (let i = 0; i < sessionReplayDomains.length; i++) { - const d = sessionReplayDomains[i]; + for (const d of sessionReplayDomains) { if (host === d || host.endsWith('.' + d)) { return true; } @@ -67,14 +66,14 @@ const offending = []; - const scripts = document.getElementsByTagName('script'); + const scripts = document.querySelectorAll('script'); for (let i = 0, len = scripts.length; i < len; i++) { if (scripts[i].src && isSessionReplayHost(scripts[i].src)) { offending.push(util.getAbsoluteURL(scripts[i].src)); } } - const iframes = document.getElementsByTagName('iframe'); + const iframes = document.querySelectorAll('iframe'); for (let i = 0, len = iframes.length; i < len; i++) { if (iframes[i].src && isSessionReplayHost(iframes[i].src)) { offending.push(util.getAbsoluteURL(iframes[i].src)); diff --git a/lib/dom/privacy/surveillance.js b/lib/dom/privacy/surveillance.js index 3e90a98..5cd7f38 100644 --- a/lib/dom/privacy/surveillance.js +++ b/lib/dom/privacy/surveillance.js @@ -66,8 +66,7 @@ if (!host) { return false; } - for (let i = 0; i < surveillanceDomains.length; i++) { - const d = surveillanceDomains[i]; + for (const d of surveillanceDomains) { if (host === d || host.endsWith('.' + d)) { return true; } @@ -77,14 +76,14 @@ const offending = []; - const scripts = document.getElementsByTagName('script'); + const scripts = document.querySelectorAll('script'); for (let i = 0, len = scripts.length; i < len; i++) { if (scripts[i].src && isSurveillanceHost(scripts[i].src)) { offending.push(util.getAbsoluteURL(scripts[i].src)); } } - const iframes = document.getElementsByTagName('iframe'); + const iframes = document.querySelectorAll('iframe'); for (let i = 0, len = iframes.length; i < len; i++) { if (iframes[i].src && isSurveillanceHost(iframes[i].src)) { offending.push(util.getAbsoluteURL(iframes[i].src)); diff --git a/lib/dom/privacy/youtube.js b/lib/dom/privacy/youtube.js index eeab04a..71b966e 100644 --- a/lib/dom/privacy/youtube.js +++ b/lib/dom/privacy/youtube.js @@ -4,7 +4,7 @@ const offending = []; let score = 100; - if (window.YT) { + if (globalThis.YT) { score = 0; } diff --git a/lib/dom/timings/elementTimings.js b/lib/dom/timings/elementTimings.js index e29233f..a88bc36 100644 --- a/lib/dom/timings/elementTimings.js +++ b/lib/dom/timings/elementTimings.js @@ -1,7 +1,7 @@ (function () { 'use strict'; const supported = PerformanceObserver.supportedEntryTypes; - if (!supported || supported.indexOf('element') === -1) { + if (!supported || !supported.includes('element')) { return; } const observer = new PerformanceObserver(() => {}); diff --git a/lib/dom/timings/fullyLoaded.js b/lib/dom/timings/fullyLoaded.js index 3fc9d69..735f135 100644 --- a/lib/dom/timings/fullyLoaded.js +++ b/lib/dom/timings/fullyLoaded.js @@ -3,8 +3,8 @@ // Let's use the Resource Timing API, so it is important that we run // this after all request/responses are finished - if (window.performance && window.performance.getEntriesByType) { - const resources = window.performance.getEntriesByType('resource'); + if (globalThis.performance && globalThis.performance.getEntriesByType) { + const resources = globalThis.performance.getEntriesByType('resource'); let max = 0; for (let i = 0, len = resources.length; i < len; i++) { diff --git a/lib/dom/timings/largestContentfulPaint.js b/lib/dom/timings/largestContentfulPaint.js index b40895d..b2d8cbd 100644 --- a/lib/dom/timings/largestContentfulPaint.js +++ b/lib/dom/timings/largestContentfulPaint.js @@ -1,14 +1,14 @@ (function () { 'use strict'; const supported = PerformanceObserver.supportedEntryTypes; - if (!supported || supported.indexOf('largest-contentful-paint') === -1) { + if (!supported || !supported.includes('largest-contentful-paint')) { return; } const observer = new PerformanceObserver(() => {}); observer.observe({ type: 'largest-contentful-paint', buffered: true }); const entries = observer.takeRecords(); if (entries.length > 0) { - const largestEntry = entries[entries.length - 1]; + const largestEntry = entries.at(-1); return { duration: largestEntry.duration, id: largestEntry.id, diff --git a/lib/dom/timings/navigationTimings.js b/lib/dom/timings/navigationTimings.js index 6716c24..8c1fc26 100644 --- a/lib/dom/timings/navigationTimings.js +++ b/lib/dom/timings/navigationTimings.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - const t = window.performance.timing; + const t = globalThis.performance.timing; const metrics = { navigationStart: 0, @@ -38,11 +38,11 @@ // Selenium converts undefined to null so lets just remove // the undefined keys - Object.keys(metrics).forEach(function (key) { + for (const key of Object.keys(metrics)) { if (metrics[key] === undefined) { delete metrics[key]; } - }); + } return metrics; })(); diff --git a/lib/dom/timings/paintTimings.js b/lib/dom/timings/paintTimings.js index 327e821..92fb156 100644 --- a/lib/dom/timings/paintTimings.js +++ b/lib/dom/timings/paintTimings.js @@ -1,10 +1,10 @@ (function () { 'use strict'; - const entries = window.performance.getEntriesByType('paint'); + const entries = globalThis.performance.getEntriesByType('paint'); const cleaned = {}; if (entries.length > 0) { - for (var i = 0; i < entries.length; i++) { - cleaned[entries[i].name] = Number(entries[i].startTime.toFixed(0)); + for (const entry of entries) { + cleaned[entry.name] = Number(entry.startTime.toFixed(0)); } return cleaned; } else return; diff --git a/lib/dom/timings/userTimings.js b/lib/dom/timings/userTimings.js index 9a5517d..4495a50 100644 --- a/lib/dom/timings/userTimings.js +++ b/lib/dom/timings/userTimings.js @@ -4,29 +4,29 @@ const measures = []; const marks = []; - if (window.performance && window.performance.getEntriesByType) { + if (globalThis.performance && globalThis.performance.getEntriesByType) { const myMarks = Array.prototype.slice.call( - window.performance.getEntriesByType('mark') + globalThis.performance.getEntriesByType('mark') ); - myMarks.forEach(function (mark) { + for (const mark of myMarks) { marks.push({ name: mark.name, startTime: mark.startTime }); - }); + } const myMeasures = Array.prototype.slice.call( - window.performance.getEntriesByType('measure') + globalThis.performance.getEntriesByType('measure') ); - myMeasures.forEach(function (measure) { + for (const measure of myMeasures) { measures.push({ name: measure.name, duration: measure.duration, startTime: measure.startTime }); - }); + } } return { diff --git a/lib/dom/util.js b/lib/dom/util.js index 3b78250..9bff32b 100644 --- a/lib/dom/util.js +++ b/lib/dom/util.js @@ -8,7 +8,7 @@ const util = { * @returns {String} the absolute URL including host/protocol. */ getAbsoluteURL: function (url) { - const a = window.document.createElement('a'); + const a = globalThis.document.createElement('a'); a.href = url; return a.href; }, @@ -20,7 +20,7 @@ const util = { * @returns {String} the hostname from the URL. */ getHostname: function (url) { - const a = window.document.createElement('a'); + const a = globalThis.document.createElement('a'); a.href = url; return a.hostname; }, @@ -34,9 +34,7 @@ const util = { * @returns {Boolean} true if the element exist. */ exists: function (element, array) { - return array.some(function (e) { - return e === element; - }); + return array.includes(element); }, /** @@ -54,7 +52,7 @@ const util = { if (attribute.toLowerCase() === attributeValue.toLowerCase()) { return item; } - return undefined; + return; }; }, @@ -90,20 +88,20 @@ const util = { getConnectionType: function () { // it's easy in Chrome if ( - window.performance.getEntriesByType('navigation') && - window.performance.getEntriesByType('navigation')[0] && - window.performance.getEntriesByType('navigation')[0].nextHopProtocol + globalThis.performance.getEntriesByType('navigation') && + globalThis.performance.getEntriesByType('navigation')[0] && + globalThis.performance.getEntriesByType('navigation')[0].nextHopProtocol ) { - return window.performance.getEntriesByType('navigation')[0] + return globalThis.performance.getEntriesByType('navigation')[0] .nextHopProtocol; } else if ( - window.performance && - window.performance.getEntriesByType && - window.performance.getEntriesByType('resource') + globalThis.performance && + globalThis.performance.getEntriesByType && + globalThis.performance.getEntriesByType('resource') ) { // if you support resource timing v2 // it's kind of easy too - const resources = window.performance.getEntriesByType('resource'); + const resources = globalThis.performance.getEntriesByType('resource'); // now we "only" need to know if it is v2 if (resources.length > 1 && resources[0].nextHopProtocol) { // if it's the same domain, say it's OK @@ -127,7 +125,7 @@ const util = { */ getSynchJSFiles: function (parentElement) { const scripts = Array.prototype.slice.call( - parentElement.getElementsByTagName('script') + parentElement.querySelectorAll('script') ); return scripts @@ -147,7 +145,7 @@ const util = { */ getAsynchJSFiles: function (parentElement) { const scripts = Array.prototype.slice.call( - parentElement.getElementsByTagName('script') + parentElement.querySelectorAll('script') ); return scripts @@ -167,7 +165,7 @@ const util = { */ getResourceHintsHrefs: function (type) { const links = Array.prototype.slice.call( - window.document.head.getElementsByTagName('link') + globalThis.document.head.querySelectorAll('link') ); return links @@ -187,7 +185,7 @@ const util = { */ getCSSFiles: function (parentElement) { const links = Array.prototype.slice.call( - parentElement.getElementsByTagName('link') + parentElement.querySelectorAll('link') ); return links @@ -212,18 +210,12 @@ const util = { * not available or the asset size is unknown we report 0. **/ getTransferSize: function (url) { - const entries = window.performance.getEntriesByName(url, 'resource'); - if (entries.length === 1 && typeof entries[0].transferSize === 'number') { - return entries[0].transferSize; - } else { - return 0; - } + const entries = globalThis.performance.getEntriesByName(url, 'resource'); + return entries.length === 1 && typeof entries[0].transferSize === 'number' + ? entries[0].transferSize + : 0; }, ms(ms) { - if (ms < 1000) { - return ms + ' ms'; - } else { - return Number(ms / 1000).toFixed(3) + ' s'; - } + return ms < 1000 ? ms + ' ms' : Number(ms / 1000).toFixed(3) + ' s'; } }; diff --git a/lib/har/bestpractice/longHeaders.js b/lib/har/bestpractice/longHeaders.js index f6b623e..e69f0df 100644 --- a/lib/har/bestpractice/longHeaders.js +++ b/lib/har/bestpractice/longHeaders.js @@ -11,7 +11,7 @@ export default { const offending = []; let score = 100; let advice = ''; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { const maxHeaderLength = 600; for (let headerName of Object.keys(asset.headers.response)) { for (let headerValue of asset.headers.response[headerName]) { @@ -28,7 +28,7 @@ export default { } } } - }); + } return { score: Math.max(0, score), diff --git a/lib/har/bestpractice/manyHeaders.js b/lib/har/bestpractice/manyHeaders.js index b3795eb..030e3fe 100644 --- a/lib/har/bestpractice/manyHeaders.js +++ b/lib/har/bestpractice/manyHeaders.js @@ -11,7 +11,7 @@ export default { const offending = []; let score = 100; let advice = ''; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { const maxHeaders = 30; // Report responses with more than 30 headers if (Object.keys(asset.headers.response).length > maxHeaders) { @@ -23,7 +23,7 @@ export default { util.plural(Object.keys(asset.headers.response).length, 'header') + '.'; } - }); + } return { score: Math.max(0, score), offending: offending, diff --git a/lib/har/bestpractice/unnecessaryHeaders.js b/lib/har/bestpractice/unnecessaryHeaders.js index 23b151c..6d1cc80 100644 --- a/lib/har/bestpractice/unnecessaryHeaders.js +++ b/lib/har/bestpractice/unnecessaryHeaders.js @@ -17,7 +17,7 @@ export default { const serverHeaders = []; let score = 100; let advice = ''; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { const headers = asset.headers.response; if (headers.p3p) { offending.push(asset.url); @@ -28,7 +28,7 @@ export default { if ( headers.expires && headers['cache-control'] && - headers['cache-control'][0].indexOf('max-age') > -1 + headers['cache-control'][0].includes('max-age') ) { // You don't need to set both expires and cache-control: max-age. Use just one! offending.push(asset.url); @@ -36,7 +36,7 @@ export default { score -= 1; } - if (headers.pragma && headers.pragma[0].indexOf('no-cache') > -1) { + if (headers.pragma && headers.pragma[0].includes('no-cache')) { offending.push(asset.url); pragmaHeaders.push(asset.url); score -= 1; @@ -56,7 +56,7 @@ export default { xframeHeaders.push(asset.url); score -= 1; } - }); + } if (p3pHeaders.length > 0) { advice += diff --git a/lib/har/harCutter.js b/lib/har/harCutter.js index eda4a6f..a78bfc1 100644 --- a/lib/har/harCutter.js +++ b/lib/har/harCutter.js @@ -8,8 +8,10 @@ function groupBy(items, key) { } export function pickAPage(har, pageIndex) { - if (typeof har.log.pages[pageIndex] !== 'undefined') { - const myHar = JSON.parse(JSON.stringify(har)); + if (har.log.pages[pageIndex] === undefined) { + throw new TypeError('PageIndex out of range'); + } else { + const myHar = structuredClone(har); // get the ID const pageId = myHar.log.pages[pageIndex].id; @@ -19,7 +21,5 @@ export function pickAPage(har, pageIndex) { myHar.log.pages = [myHar.log.pages[pageIndex]]; return myHar; - } else { - throw new Error('PageIndex out of range'); } } diff --git a/lib/har/index.js b/lib/har/index.js index b8ce97a..c6c187a 100644 --- a/lib/har/index.js +++ b/lib/har/index.js @@ -14,7 +14,7 @@ export function runAdvice(pages, harAdvicesByCategory, domAdvice, options) { for (let category of Object.keys(harAdvicesByCategory)) { const adviceList = harAdvicesByCategory[category]; if (category !== 'info') { - const allAdvice = adviceList.filter(validateAdvice); + const allAdvice = adviceList.filter((advice) => validateAdvice(advice)); results[category] = { adviceList: {} }; @@ -83,7 +83,9 @@ function validateAdvice(advice) { throw new Error('Advice is missing an id: ', JSON.stringify(advice)); } if (!(typeof advice.processPage === 'function')) { - throw new Error('Advice ' + advice.id + ' needs a processPage function.'); + throw new TypeError( + 'Advice ' + advice.id + ' needs a processPage function.' + ); } return true; } diff --git a/lib/har/info/technology.js b/lib/har/info/technology.js index 157a325..9a90c4e 100644 --- a/lib/har/info/technology.js +++ b/lib/har/info/technology.js @@ -15,10 +15,10 @@ let technologies = {}; // Define the length of the array we will iterate over const arrayLength = 27; -for (const index of Array(arrayLength).keys()) { +for (const index of Array.from({ length: arrayLength }).keys()) { // Determine the character representation for this index // If index is 0, use '_', else use ASCII character corresponding to (index + 96) - const character = index ? String.fromCharCode(index + 96) : '_'; + const character = index ? String.fromCodePoint(index + 96) : '_'; const filePath = resolve(`${__dirname}/../../technologies/${character}.json`); @@ -103,8 +103,7 @@ export default { html }); return Wappalyzer.resolve(detections); - // eslint-disable-next-line no-unused-vars - } catch (e) { + } catch { return {}; } } diff --git a/lib/har/performance/assetsRedirects.js b/lib/har/performance/assetsRedirects.js index 99bf964..38090bd 100644 --- a/lib/har/performance/assetsRedirects.js +++ b/lib/har/performance/assetsRedirects.js @@ -1,9 +1,9 @@ import * as util from '../util.js'; -var redirect = [301, 302, 303, 305, 306, 307]; +var redirect = new Set([301, 302, 303, 305, 306, 307]); function isRedirect(asset) { - return redirect.indexOf(asset.status) > -1; + return redirect.has(asset.status); } export default { @@ -18,7 +18,7 @@ export default { let score = 100; let offending = []; let sameDomainAsDocument = 0; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { // skip the main document if that is redirected // it's caught the ine documentRedirect advice if (page.url !== asset.url && isRedirect(asset)) { @@ -28,7 +28,7 @@ export default { sameDomainAsDocument++; } } - }); + } return { score: Math.max(0, score), diff --git a/lib/har/performance/cacheHeaders.js b/lib/har/performance/cacheHeaders.js index 09f41cf..e1432af 100644 --- a/lib/har/performance/cacheHeaders.js +++ b/lib/har/performance/cacheHeaders.js @@ -1,10 +1,10 @@ import * as util from '../util.js'; -const SKIPPABLE_DOMAINS = [ +const SKIPPABLE_DOMAINS = new Set([ 'www.google-analytics.com', 'ssl.google-analytics.com', 'analytics.twitter.com' -]; +]); export default { id: 'cacheHeaders', @@ -19,15 +19,15 @@ export default { let score = 100; let offending = []; let saveSize = 0; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { // Don't check the main page/document since it is common to not // cache that if (asset.url === page.finalUrl) { - return; + continue; } if ( - SKIPPABLE_DOMAINS.indexOf(util.getHostname(asset.url)) === -1 && + !SKIPPABLE_DOMAINS.has(util.getHostname(asset.url)) && asset.expires <= 0 && asset.method === 'GET' ) { @@ -36,7 +36,7 @@ export default { saveSize += asset.transferSize; offending.push(asset.url); } - }); + } return { score: Math.max(0, score), diff --git a/lib/har/performance/cacheHeadersLong.js b/lib/har/performance/cacheHeadersLong.js index cedd5df..e7105d5 100644 --- a/lib/har/performance/cacheHeadersLong.js +++ b/lib/har/performance/cacheHeadersLong.js @@ -1,19 +1,19 @@ import * as util from '../util.js'; -const SKIPPABLE_DOMAINS = [ +const SKIPPABLE_DOMAINS = new Set([ 'www.google-analytics.com', 'ssl.google-analytics.com', 'analytics.twitter.com' -]; +]); // One year in seconds — the cache lifetime modern best practice (and the // HTTP/1.1 spec) recommends as the upper bound. For content-hashed URLs // (e.g. /static/app.4af2.css) this is what `Cache-Control: // max-age=31536000, immutable` is designed for. -const ONE_YEAR = 31536000; +const ONE_YEAR = 31_536_000; function processAsset(asset) { - if (SKIPPABLE_DOMAINS.indexOf(util.getHostname(asset.url)) > -1) { + if (SKIPPABLE_DOMAINS.has(util.getHostname(asset.url))) { return 0; } else if (asset.expires >= ONE_YEAR) { return 0; @@ -38,18 +38,18 @@ export default { processPage: function (page) { let score = 100; let offending = []; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { // Don't check the main page/document since it is common to not // cache that if (asset.url === page.finalUrl) { - return; + continue; } let myScore = processAsset(asset); if (myScore > 0) { score -= myScore; offending.push(asset.url); } - }); + } return { score: Math.max(0, score), diff --git a/lib/har/performance/compressAssets.js b/lib/har/performance/compressAssets.js index 220e2e5..65aad79 100644 --- a/lib/har/performance/compressAssets.js +++ b/lib/har/performance/compressAssets.js @@ -1,8 +1,8 @@ import * as util from '../util.js'; -const types = ['html', 'plain', 'json', 'javascript', 'css', 'svg']; +const types = new Set(['html', 'plain', 'json', 'javascript', 'css', 'svg']); function processAsset(asset) { - if (types.indexOf(asset.type) > -1) { + if (types.has(asset.type)) { const headers = asset.headers.response; const encoding = headers['content-encoding'] ? headers['content-encoding'][0] @@ -32,7 +32,7 @@ export default { processPage: function (page) { let score = 100; let offending = []; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { let myScore = processAsset(asset); if (myScore > 0) { score -= myScore; @@ -42,7 +42,7 @@ export default { contentSize: asset.contentSize }); } - }); + } return { score: Math.max(0, score), diff --git a/lib/har/performance/connectionKeepAlive.js b/lib/har/performance/connectionKeepAlive.js index 9c249d5..969e589 100644 --- a/lib/har/performance/connectionKeepAlive.js +++ b/lib/har/performance/connectionKeepAlive.js @@ -14,21 +14,21 @@ export default { let avoid = 'doc'; let closedPerDomain = {}; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { const headers = asset.headers.response; const connectionHeader = headers.connection ? headers.connection[0] : ''; - if (asset.type !== avoid && connectionHeader.indexOf('close') > -1) { + if (asset.type !== avoid && connectionHeader.includes('close')) { const hostname = util.getHostname(asset.url); - if (!closedPerDomain[hostname]) { - closedPerDomain[hostname] = 1; - // TODO these assets should be reported too - } else { + if (closedPerDomain[hostname]) { closedPerDomain[hostname] += 1; offending.push(asset.url); score -= 10; + } else { + closedPerDomain[hostname] = 1; + // TODO these assets should be reported too } } - }); + } return { score: Math.max(0, score), offending: offending, diff --git a/lib/har/performance/cssSize.js b/lib/har/performance/cssSize.js index c0e9dc7..ec4b141 100644 --- a/lib/har/performance/cssSize.js +++ b/lib/har/performance/cssSize.js @@ -13,13 +13,13 @@ export default { let cssAssets = page.assets.filter((asset) => asset.type === 'css'); let transferSize = 0; let contentSize = 0; - let transferLimit = 150000; - let contentLimit = 500000; + let transferLimit = 150_000; + let contentLimit = 500_000; let score = 100; let advice = ''; const offending = []; - cssAssets.forEach(function (asset) { + for (const asset of cssAssets) { transferSize += asset.transferSize; contentSize += asset.contentSize; offending.push({ @@ -27,7 +27,7 @@ export default { transferSize: asset.transferSize, contentSize: asset.contentSize }); - }); + } if (transferSize > transferLimit) { score -= 50; @@ -46,10 +46,10 @@ export default { : '') + '. '; - if (contentSize > 1000000 && options.mobile) { + if (contentSize > 1_000_000 && options.mobile) { advice += "Wow, it's insane amount of CSS to render a page on a mobile device."; - } else if (contentSize > 2000000) { + } else if (contentSize > 2_000_000) { advice += "I don't know what to say, the page needs more than 2 MB CSS to render."; } else { diff --git a/lib/har/performance/documentRedirect.js b/lib/har/performance/documentRedirect.js index ae46f15..83ef4e4 100644 --- a/lib/har/performance/documentRedirect.js +++ b/lib/har/performance/documentRedirect.js @@ -16,15 +16,14 @@ export default { : ''; // if a HTTP redirects to HTTPS don't hurt that because that is // really nice :) - if (page.documentRedirects === 1 && page.url !== page.finalUrl) { - // do we redirect to an HTTP page? - if ( - page.url.indexOf('http:') > -1 && - page.finalUrl.indexOf('https') > -1 - ) { - advice = "The page redirects from HTTP to HTTPS, that's good!"; - score = 100; - } + if ( + page.documentRedirects === 1 && + page.url !== page.finalUrl && // do we redirect to an HTTP page? + page.url.includes('http:') && + page.finalUrl.includes('https') + ) { + advice = "The page redirects from HTTP to HTTPS, that's good!"; + score = 100; } return { diff --git a/lib/har/performance/favicon.js b/lib/har/performance/favicon.js index 54f2552..822d3bc 100644 --- a/lib/har/performance/favicon.js +++ b/lib/har/performance/favicon.js @@ -13,13 +13,13 @@ export default { let total = 0; const offending = []; let advice = ''; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.type === 'favicon') { if (asset.status >= 299) { total += 100; advice += 'The favicon returned ' + asset.status + '. '; } - if (asset.transferSize > 10000) { + if (asset.transferSize > 10_000) { total += 50; advice += 'The favicon size is ' + @@ -34,7 +34,7 @@ export default { offending.push(asset.url); } } - }); + } // If we miss out of the favicon ... well do not report it for now // it seems to be a bug in how we create the HAR in Chrome (or in PageXray) diff --git a/lib/har/performance/fewFonts.js b/lib/har/performance/fewFonts.js index d74dc89..1a1b100 100644 --- a/lib/har/performance/fewFonts.js +++ b/lib/har/performance/fewFonts.js @@ -14,12 +14,7 @@ export default { .map((asset) => asset.url); // only hurt if we got more than one font - let score = - urls.length > 1 - ? 100 - urls.length * 10 < 0 - ? 0 - : 100 - urls.length * 10 - : 100; + let score = urls.length > 1 ? Math.max(0, 100 - urls.length * 10) : 100; return { score: score, diff --git a/lib/har/performance/fewRequestsPerDomain.js b/lib/har/performance/fewRequestsPerDomain.js index e6407f5..e1e15ab 100644 --- a/lib/har/performance/fewRequestsPerDomain.js +++ b/lib/har/performance/fewRequestsPerDomain.js @@ -48,13 +48,13 @@ export default { limit + ' requests. '; - Object.keys(domainAndRequests).forEach(function (domain) { + for (const domain of Object.keys(domainAndRequests)) { advice += domain + ' got ' + util.plural(domainAndRequests[domain], 'request') + '. '; - }); + } advice += 'Improve performance by sharding those or move to HTTP/2.'; } diff --git a/lib/har/performance/headerSize.js b/lib/har/performance/headerSize.js index c8e5f9a..3d0089d 100644 --- a/lib/har/performance/headerSize.js +++ b/lib/har/performance/headerSize.js @@ -26,15 +26,15 @@ export default { "The page uses an HTTP/3 connection and the headers are sent compressed, that's good. The current coach version cannot say if the size is good or not." }; } else { - let limit = 20000; + let limit = 20_000; let score = 100; let offending = []; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.headerSize > limit) { offending.push(asset.url); score -= 10; } - }); + } return { score: Math.max(0, score), offending: offending, diff --git a/lib/har/performance/imageSize.js b/lib/har/performance/imageSize.js index f87f196..c88cb4c 100644 --- a/lib/har/performance/imageSize.js +++ b/lib/har/performance/imageSize.js @@ -14,12 +14,12 @@ export default { (asset) => asset.type === 'image' || asset.type === 'svg' ); let contentSize = 0; - let contentLimit = 700000; + let contentLimit = 700_000; let score = 100; - images.forEach(function (image) { + for (const image of images) { contentSize += image.contentSize; - }); + } if (contentSize > contentLimit) { // TODO reduce the score per 100 kB. diff --git a/lib/har/performance/javascriptSize.js b/lib/har/performance/javascriptSize.js index 9bfe902..4b6d237 100644 --- a/lib/har/performance/javascriptSize.js +++ b/lib/har/performance/javascriptSize.js @@ -13,12 +13,12 @@ export default { let jsAssets = page.assets.filter((asset) => asset.type === 'javascript'); let transferSize = 0; let contentSize = 0; - let transferLimit = 250000; - let contentLimit = 800000; + let transferLimit = 250_000; + let contentLimit = 800_000; let score = 100; const offending = []; - jsAssets.forEach(function (asset) { + for (const asset of jsAssets) { transferSize += asset.transferSize; contentSize += asset.contentSize; offending.push({ @@ -26,7 +26,7 @@ export default { transferSize: asset.transferSize, contentSize: asset.contentSize }); - }); + } if (transferSize > transferLimit) { score -= 50; @@ -47,7 +47,7 @@ export default { ? ' and the uncompressed size is ' + util.formatBytes(contentSize) : '') + '. ' + - (contentSize > 1000000 + (contentSize > 1_000_000 ? 'This is totally crazy! There is really room for improvement here. ' : 'This is quite large. ') + '' diff --git a/lib/har/performance/mimeTypes.js b/lib/har/performance/mimeTypes.js index eb19205..e72c85b 100644 --- a/lib/har/performance/mimeTypes.js +++ b/lib/har/performance/mimeTypes.js @@ -11,7 +11,7 @@ export default { processPage: function (page) { let score = 100; let offending = []; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if ( asset.type == 'other' && asset.status > 199 && @@ -21,7 +21,7 @@ export default { score--; offending.push(asset.url); } - }); + } return { score: Math.max(0, score), offending: offending, diff --git a/lib/har/performance/optimalCssSize.js b/lib/har/performance/optimalCssSize.js index 99e01ee..54d8207 100644 --- a/lib/har/performance/optimalCssSize.js +++ b/lib/har/performance/optimalCssSize.js @@ -5,7 +5,7 @@ import * as util from '../util.js'; // response above ~25 KB is usually a sign of mixed critical and // non-critical styles being shipped together — split out the critical // rules and lazy-load the rest. -const TRANSFER_LIMIT = 25000; +const TRANSFER_LIMIT = 25_000; export default { id: 'optimalCssSize', @@ -22,7 +22,7 @@ export default { let advice = ''; let offending = []; - cssAssets.forEach(function (asset) { + for (const asset of cssAssets) { if (asset.transferSize > TRANSFER_LIMIT) { score -= 10; offending.push({ @@ -40,7 +40,7 @@ export default { util.formatBytes(TRANSFER_LIMIT) + '. '; } - }); + } if (score < 100) { advice += diff --git a/lib/har/performance/pageSize.js b/lib/har/performance/pageSize.js index d6f1f73..c0dbc45 100644 --- a/lib/har/performance/pageSize.js +++ b/lib/har/performance/pageSize.js @@ -9,16 +9,16 @@ export default { severity: 'warn', tags: ['performance', 'mobile'], processPage: function (page, domAdvice, options) { - let sizeLimit = options.mobile ? 2000000 : 3000000; + let sizeLimit = options.mobile ? 2_000_000 : 3_000_000; if (page.transferSize > sizeLimit) { const offending = []; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { offending.push({ url: asset.url, transferSize: asset.transferSize, contentSize: asset.contentSize }); - }); + } return { score: 0, offending, @@ -28,7 +28,7 @@ export default { ', which is more than the coach limit of ' + util.formatBytes(sizeLimit) + '. That is ' + - (page.transferSize > 5000000 ? 'insane' : 'really big') + + (page.transferSize > 5_000_000 ? 'insane' : 'really big') + ' and you need to make it smaller.' }; } diff --git a/lib/har/performance/privateAssets.js b/lib/har/performance/privateAssets.js index c4fb658..bd0885b 100644 --- a/lib/har/performance/privateAssets.js +++ b/lib/har/performance/privateAssets.js @@ -1,13 +1,21 @@ import * as util from '../util.js'; -const types = ['json', 'javascript', 'css', 'image', 'svg', 'font', 'html']; +const types = new Set([ + 'json', + 'javascript', + 'css', + 'image', + 'svg', + 'font', + 'html' +]); function processAsset(asset) { - if (types.indexOf(asset.type) > -1 && asset.method === 'GET') { + if (types.has(asset.type) && asset.method === 'GET') { const headers = asset.headers.response; const cacheControl = headers['cache-control'] ? headers['cache-control'][0] : ''; - if (cacheControl.indexOf('private') > -1) { + if (cacheControl.includes('private')) { return 10; } } @@ -26,7 +34,7 @@ export default { let advice = ''; let score = 100; let offending = []; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { let myScore = processAsset(asset); if (myScore > 0 && page.url !== asset.url) { score -= myScore; @@ -36,7 +44,7 @@ export default { advice = 'The main page has a private header. It could be right in some cases where the user can be logged in and served specific content. But if your asset is static it should never be private. '; } - }); + } return { score: Math.max(0, score), diff --git a/lib/har/performance/responseOk.js b/lib/har/performance/responseOk.js index 128e506..da72464 100644 --- a/lib/har/performance/responseOk.js +++ b/lib/har/performance/responseOk.js @@ -13,7 +13,7 @@ export default { let offending = []; let offendingCodes = {}; let advice = ''; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.status >= 400) { offending.push(asset.url); score -= 10; @@ -23,21 +23,21 @@ export default { offendingCodes[asset.status] = 1; } } - }); + } if (score < 100) { advice = 'The page has ' + util.plural(offending.length, 'error response') + '. '; - Object.keys(offendingCodes).forEach(function (errorCode) { + for (const errorCode of Object.keys(offendingCodes)) { advice += 'The page has ' + util.plural(offendingCodes[errorCode], 'response') + ' with code ' + errorCode + '. '; - }); + } } return { diff --git a/lib/har/privacy/contentSecurityPolicyHeader.js b/lib/har/privacy/contentSecurityPolicyHeader.js index f6d1ce3..25934ed 100644 --- a/lib/har/privacy/contentSecurityPolicyHeader.js +++ b/lib/har/privacy/contentSecurityPolicyHeader.js @@ -38,7 +38,7 @@ const DEFAULT_SRC_NONE_RE = /(?:^|;)\s*default-src\s+[^;]*'none'/i; function readHeader(asset) { const raw = asset.headers.response['content-security-policy']; - if (!raw) return undefined; + if (!raw) return; // pagexray normalises headers to arrays of values per name. return Array.isArray(raw) ? raw[0] : raw; } @@ -57,9 +57,9 @@ export default { let score = 0; const adviceParts = []; const finalUrl = page.finalUrl; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.url !== finalUrl) { - return; + continue; } const value = readHeader(asset); if (!value) { @@ -67,7 +67,7 @@ export default { adviceParts.push( 'Set a Content-Security-Policy header to mitigate cross-site scripting attacks. You can start with a Content-Security-Policy-Report-Only header, which only reports violations rather than blocking them.' ); - return; + continue; } score = 100; @@ -75,16 +75,14 @@ export default { const hasNonceOrHash = NONCE_RE.test(value) || HASH_RE.test(value); const looksStrict = hasStrictDynamic && hasNonceOrHash; - if (UNSAFE_INLINE_RE.test(value)) { - if (!looksStrict) { - score -= 40; - adviceParts.push( - "The policy allows 'unsafe-inline', which lets the browser execute inline scripts and styles directly from the page. Move to nonces or hashes plus 'strict-dynamic' so that inline injection cannot run." - ); - } - // If looksStrict: modern browsers ignore unsafe-inline when - // strict-dynamic + nonces/hashes are present, so no penalty. + if (UNSAFE_INLINE_RE.test(value) && !looksStrict) { + score -= 40; + adviceParts.push( + "The policy allows 'unsafe-inline', which lets the browser execute inline scripts and styles directly from the page. Move to nonces or hashes plus 'strict-dynamic' so that inline injection cannot run." + ); } + // If looksStrict: modern browsers ignore unsafe-inline when + // strict-dynamic + nonces/hashes are present, so no penalty. if (UNSAFE_EVAL_RE.test(value)) { score -= 30; @@ -114,7 +112,7 @@ export default { "The policy uses 'strict-dynamic' together with a nonce or hash — that is the recommended modern shape. Good." ); } - }); + } if (score < 0) { score = 0; diff --git a/lib/har/privacy/crossOriginEmbedderPolicyHeader.js b/lib/har/privacy/crossOriginEmbedderPolicyHeader.js index a4f832d..0cdd6c1 100644 --- a/lib/har/privacy/crossOriginEmbedderPolicyHeader.js +++ b/lib/har/privacy/crossOriginEmbedderPolicyHeader.js @@ -12,7 +12,7 @@ export default { let score = 0; let advice = ''; const finalUrl = page.finalUrl; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.url === finalUrl) { const headers = asset.headers.response; if (headers['cross-origin-embedder-policy']) { @@ -21,7 +21,7 @@ export default { offending.push(asset.url); } } - }); + } if (score === 0) { advice = 'Set a Cross-Origin-Embedder-Policy header (typically require-corp or credentialless) on the document response to control cross-origin embedding.'; diff --git a/lib/har/privacy/crossOriginOpenerPolicyHeader.js b/lib/har/privacy/crossOriginOpenerPolicyHeader.js index 6b02806..3befb14 100644 --- a/lib/har/privacy/crossOriginOpenerPolicyHeader.js +++ b/lib/har/privacy/crossOriginOpenerPolicyHeader.js @@ -12,7 +12,7 @@ export default { let score = 0; let advice = ''; const finalUrl = page.finalUrl; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.url === finalUrl) { const headers = asset.headers.response; if (headers['cross-origin-opener-policy']) { @@ -21,7 +21,7 @@ export default { offending.push(asset.url); } } - }); + } if (score === 0) { advice = 'Set a Cross-Origin-Opener-Policy header (typically same-origin) on the document response to isolate the page from cross-origin windows.'; diff --git a/lib/har/privacy/crossOriginResourcePolicyHeader.js b/lib/har/privacy/crossOriginResourcePolicyHeader.js index 8a932df..ef7d6da 100644 --- a/lib/har/privacy/crossOriginResourcePolicyHeader.js +++ b/lib/har/privacy/crossOriginResourcePolicyHeader.js @@ -12,7 +12,7 @@ export default { let score = 0; let advice = ''; const finalUrl = page.finalUrl; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.url === finalUrl) { const headers = asset.headers.response; if (headers['cross-origin-resource-policy']) { @@ -21,7 +21,7 @@ export default { offending.push(asset.url); } } - }); + } if (score === 0) { advice = 'Set a Cross-Origin-Resource-Policy header (same-origin, same-site or cross-origin) on the document response to limit who may embed it.'; diff --git a/lib/har/privacy/googleReCaptcha.js b/lib/har/privacy/googleReCaptcha.js index e22c420..72392c4 100644 --- a/lib/har/privacy/googleReCaptcha.js +++ b/lib/har/privacy/googleReCaptcha.js @@ -11,16 +11,16 @@ export default { let score = 100; let advice = ''; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { // Avoid catching redirects from http to https if ( - asset.url.indexOf('www.google.com/recaptcha/enterprise.js') > -1 || - asset.url.indexOf('www.google.com/recaptcha/api.js') > -1 + asset.url.includes('www.google.com/recaptcha/enterprise.js') || + asset.url.includes('www.google.com/recaptcha/api.js') ) { score = 0; offending.push(asset.url); } - }); + } if (score === 0) { advice = diff --git a/lib/har/privacy/mixedContent.js b/lib/har/privacy/mixedContent.js index 0b5a74d..42fbf2e 100644 --- a/lib/har/privacy/mixedContent.js +++ b/lib/har/privacy/mixedContent.js @@ -12,14 +12,14 @@ export default { let score = 100; let advice = ''; const finalUrl = page.finalUrl; - if (finalUrl.indexOf('https://') > -1) { - page.assets.forEach(function (asset) { + if (finalUrl.includes('https://')) { + for (const asset of page.assets) { // Avoid catching redirects from http to https - if (asset.url.indexOf('http://') > -1 && asset.url !== page.url) { + if (asset.url.includes('http://') && asset.url !== page.url) { score = 0; offending.push(asset.url); } - }); + } } if (score === 0) { advice = diff --git a/lib/har/privacy/nelHeader.js b/lib/har/privacy/nelHeader.js index 2f64b6f..996e259 100644 --- a/lib/har/privacy/nelHeader.js +++ b/lib/har/privacy/nelHeader.js @@ -11,7 +11,7 @@ export default { let score = 0; let advice = ''; const finalUrl = page.finalUrl; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.url === finalUrl) { const headers = asset.headers.response; if (headers['nel']) { @@ -20,7 +20,7 @@ export default { offending.push(asset.url); } } - }); + } if (score === 0) { advice = 'Set a NEL header (paired with Reporting-Endpoints) to collect connection-level error reports from the field.'; diff --git a/lib/har/privacy/permissionsPolicyHeader.js b/lib/har/privacy/permissionsPolicyHeader.js index 16b77f3..c0fb213 100644 --- a/lib/har/privacy/permissionsPolicyHeader.js +++ b/lib/har/privacy/permissionsPolicyHeader.js @@ -12,7 +12,7 @@ export default { let score = 0; let advice = ''; const finalUrl = page.finalUrl; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.url === finalUrl) { const headers = asset.headers.response; if (headers['permissions-policy'] || headers['feature-policy']) { @@ -21,7 +21,7 @@ export default { offending.push(asset.url); } } - }); + } if (score === 0) { advice = 'Set a Permissions-Policy header to control which browser features the page can use.'; diff --git a/lib/har/privacy/referrerPolicyHeader.js b/lib/har/privacy/referrerPolicyHeader.js index 1e13db4..8f493ff 100644 --- a/lib/har/privacy/referrerPolicyHeader.js +++ b/lib/har/privacy/referrerPolicyHeader.js @@ -12,7 +12,7 @@ export default { let score = 0; let advice = ''; const finalUrl = page.finalUrl; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.url === finalUrl) { const headers = asset.headers.response; if (headers['referrer-policy']) { @@ -21,7 +21,7 @@ export default { offending.push(asset.url); } } - }); + } if (score === 0) { advice = 'Set a referrer-policy header to make sure you do not leak user information.'; diff --git a/lib/har/privacy/reportingEndpointsHeader.js b/lib/har/privacy/reportingEndpointsHeader.js index 9b7efc9..07751a6 100644 --- a/lib/har/privacy/reportingEndpointsHeader.js +++ b/lib/har/privacy/reportingEndpointsHeader.js @@ -12,7 +12,7 @@ export default { let score = 0; let advice = ''; const finalUrl = page.finalUrl; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.url === finalUrl) { const headers = asset.headers.response; // Reporting-Endpoints is the modern shape; Report-To is the @@ -23,7 +23,7 @@ export default { offending.push(asset.url); } } - }); + } if (score === 0) { advice = 'Set a Reporting-Endpoints header (or the legacy Report-To header) so CSP reports, NEL data and other Reporting-API events have an endpoint to land at.'; diff --git a/lib/har/privacy/strictTransportSecurityHeader.js b/lib/har/privacy/strictTransportSecurityHeader.js index a194e05..18e0892 100644 --- a/lib/har/privacy/strictTransportSecurityHeader.js +++ b/lib/har/privacy/strictTransportSecurityHeader.js @@ -12,37 +12,35 @@ export default { let score = 100; let advice = ''; const finalUrl = page.finalUrl; - if (finalUrl.indexOf('https://') > -1) { - page.assets.forEach(function (asset) { + if (finalUrl.includes('https://')) { + for (const asset of page.assets) { if (asset.url === finalUrl) { const headers = asset.headers.response; if (headers['strict-transport-security']) { const h = headers['strict-transport-security'][0]; - if (h.indexOf('includeSubDomains') === -1) { + if (!h.includes('includeSubDomains')) { score = 90; advice = 'A strict transport header is set but miss out on setting includeSubDomains'; } - if (h.indexOf('max-age=') === -1) { - score = 0; - advice = - 'A strict transport header is set but but no max-age! The header is not set correct.'; - } else { + if (h.includes('max-age=')) { const parts = h.split(';'); if (parts[0].startsWith('max-age=')) { - const time = Number( - parts[0].substring(parts[0].indexOf('=') + 1, parts[0].length) - ); + const time = Number(parts[0].slice(parts[0].indexOf('=') + 1)); // The HSTS preload list (https://hstspreload.org) // requires max-age >= 31536000 (one year). Anything below // that is below the practical security baseline. - const minOneYear = 31536000; + const minOneYear = 31_536_000; if (time < minOneYear) { score -= 20; advice += 'The max-age is lower than one year, which is below the HSTS preload list minimum. Set it to at least 31536000 (one year), and 63072000 (two years) is recommended.'; } } + } else { + score = 0; + advice = + 'A strict transport header is set but but no max-age! The header is not set correct.'; } } else { score = 0; @@ -51,7 +49,7 @@ export default { 'Set a strict transport header to make sure the user always use HTTPS.'; } } - }); + } } return { score: score, diff --git a/lib/har/privacy/thirdPartyPrivacy.js b/lib/har/privacy/thirdPartyPrivacy.js index 0a06c45..3514c62 100644 --- a/lib/har/privacy/thirdPartyPrivacy.js +++ b/lib/har/privacy/thirdPartyPrivacy.js @@ -26,19 +26,16 @@ export default { let score = 100 - Math.round(thirdPartyPercent); let advice; - if (thirdPartyPercent === 100) { - advice = - 'The page has 100% of third party requests! Something is probably wrong? Verify that you test the right page.'; - } else { - advice = - 'The page has ' + - Math.round(thirdPartyPercent) + - '% requests that are 3rd party (' + - thirdPartyRequests + - ' requests with a size of ' + - thirdPartySize + - ').'; - } + advice = + thirdPartyPercent === 100 + ? 'The page has 100% of third party requests! Something is probably wrong? Verify that you test the right page.' + : 'The page has ' + + Math.round(thirdPartyPercent) + + '% requests that are 3rd party (' + + thirdPartyRequests + + ' requests with a size of ' + + thirdPartySize + + ').'; if ( thirdParties.byCategory['surveillance'] && @@ -51,7 +48,7 @@ export default { const doNotInclude = 'other cdn '; for (let category of Object.keys(thirdParties.byCategory)) { - if (doNotInclude.indexOf(category) === -1) { + if (!doNotInclude.includes(category)) { advice += ' The page do ' + util.plural( diff --git a/lib/har/privacy/xContentTypeOptionsHeader.js b/lib/har/privacy/xContentTypeOptionsHeader.js index d5ab47b..610bd98 100644 --- a/lib/har/privacy/xContentTypeOptionsHeader.js +++ b/lib/har/privacy/xContentTypeOptionsHeader.js @@ -12,7 +12,7 @@ export default { let score = 0; let advice = ''; const finalUrl = page.finalUrl; - page.assets.forEach(function (asset) { + for (const asset of page.assets) { if (asset.url === finalUrl) { const headers = asset.headers.response; const value = @@ -24,7 +24,7 @@ export default { offending.push(asset.url); } } - }); + } if (score === 0) { advice = 'Set X-Content-Type-Options: nosniff on the document response to prevent MIME-sniffing.'; diff --git a/lib/har/thirdParty.js b/lib/har/thirdParty.js index bf0ca45..a5cf6a0 100644 --- a/lib/har/thirdParty.js +++ b/lib/har/thirdParty.js @@ -11,11 +11,8 @@ export function getThirdParty(page) { let company; try { company = - page && page.url.indexOf('localhost') > -1 - ? undefined - : getEntity(page.url); - // eslint-disable-next-line no-unused-vars - } catch (e) { + page && page.url.includes('localhost') ? undefined : getEntity(page.url); + } catch { // https://github.com/sitespeedio/sitespeed.io/issues/3043 } @@ -24,12 +21,11 @@ export function getThirdParty(page) { let entity; try { entity = - (asset && asset.url.indexOf('localhost') > -1) || + (asset && asset.url.includes('localhost')) || (asset && !asset.url.startsWith('http')) ? undefined : getEntity(asset.url); - // eslint-disable-next-line no-unused-vars - } catch (e) { + } catch { continue; } if (entity !== undefined) { @@ -45,14 +41,13 @@ export function getThirdParty(page) { } let categories = entity.categories; if ( - entity.name.indexOf('Google') > -1 || - entity.name.indexOf('Facebook') > -1 || - entity.name.indexOf('AMP') > -1 || - entity.name.indexOf('YouTube') > -1 + (entity.name.includes('Google') || + entity.name.includes('Facebook') || + entity.name.includes('AMP') || + entity.name.includes('YouTube')) && + !categories.includes('surveillance') ) { - if (!categories.includes('surveillance')) { - categories = categories.concat('surveillance'); - } + categories = categories.concat('surveillance'); } for (let category of categories) { if (!toolsByCategory[category]) { diff --git a/lib/har/util.js b/lib/har/util.js index 703e00a..9cf9fc7 100644 --- a/lib/har/util.js +++ b/lib/har/util.js @@ -33,7 +33,7 @@ export function formatBytes(bytes) { return '0 B'; } - var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1000)), 10); + var i = Number.parseInt(Math.floor(Math.log(bytes) / Math.log(1000)), 10); return Math.round((bytes / Math.pow(1000, i)) * 10) / 10 + ' ' + sizes[i]; } @@ -43,7 +43,7 @@ export function formatBytes(bytes) { * @return {String} The time in nearest largest definition. */ export function prettyPrintSeconds(seconds) { - if (typeof seconds === 'undefined') { + if (seconds === undefined) { return ''; } @@ -102,20 +102,20 @@ export function prettyPrintSeconds(seconds) { export function human(node, pretty) { if (!node) { - return undefined; + return; } let clean = {}; - Object.keys(node).forEach(function (key) { + for (const key of Object.keys(node)) { clean[key] = pretty(node[key]); - }); + } return clean; } export function shortURL(url) { if (url.length > 40) { let shortUrl = url.replace(/\?.*/, ''); - url = shortUrl.substr(0, 20) + '...' + shortUrl.substr(-17); + url = shortUrl.slice(0, 20) + '...' + shortUrl.slice(-17); } return url; } diff --git a/lib/index.js b/lib/index.js index 8db8fc9..ec41c87 100644 --- a/lib/index.js +++ b/lib/index.js @@ -21,6 +21,19 @@ function getPagesFromHar(harJson, options) { }); } +async function getFolders(rootPath) { + const result = []; + const files = await readdir(rootPath); + for (let file of files) { + const filePath = rootPath + '/' + file; + const stats = await stat(filePath); + if (stats.isDirectory()) { + result.push(file); + } + } + return result; +} + export async function getDomAdvice() { return readFile(resolve(__dirname, '..', 'dist', 'coach.min.js'), 'utf8'); } @@ -32,19 +45,6 @@ export async function getHarAdvice() { // har/performance/advice.js // files.js const harRootPath = resolve(__dirname, 'har'); - - async function getFolders(rootPath) { - const result = []; - const files = await readdir(rootPath); - for (let file of files) { - const filePath = rootPath + '/' + file; - if ((await stat(filePath)).isDirectory()) { - result.push(file); - } - } - return result; - } - const folders = await getFolders(harRootPath); const harAdvice = {}; for (let folderName of folders) { diff --git a/lib/merge.js b/lib/merge.js index 4f98197..8489691 100644 --- a/lib/merge.js +++ b/lib/merge.js @@ -5,14 +5,14 @@ function recalculateScore(results) { var totalScore = 0, totalWeight = 0; - Object.keys(results.advice).forEach((result) => { + for (const result of Object.keys(results.advice)) { const category = results.advice[result]; var categoryScore = 0, categoryWeight = 0, adviceList = category.adviceList; if (adviceList) { - Object.keys(adviceList).forEach((adviceName) => { + for (const adviceName of Object.keys(adviceList)) { var advice = adviceList[adviceName]; // Backfill severity on any rule that didn't declare one. This covers @@ -27,12 +27,12 @@ function recalculateScore(results) { totalWeight += advice.weight; categoryWeight += advice.weight; - }); + } } if (categoryWeight > 0) { category.score = Math.round(categoryScore / categoryWeight); } - }); + } results.advice.score = Math.round(totalScore / totalWeight); return results; diff --git a/lib/technologies/wappalyzer.js b/lib/technologies/wappalyzer.js index c13c4d3..84b7618 100644 --- a/lib/technologies/wappalyzer.js +++ b/lib/technologies/wappalyzer.js @@ -18,9 +18,9 @@ function toArray(value) { function slugify(string) { return String(string) .toLowerCase() - .replace(/[^a-z0-9-]/g, '-') - .replace(/--+/g, '-') - .replace(/(?:^-|-$)/g, ''); + .replaceAll(/[^a-z0-9-]/g, '-') + .replaceAll(/--+/g, '-') + .replaceAll(/(?:^-|-$)/g, ''); } function parsePattern(pattern, isRegex = true) { @@ -29,17 +29,13 @@ function parsePattern(pattern, isRegex = true) { pattern !== null && !Array.isArray(pattern) ) { - return Object.keys(pattern).reduce( - (parsed, key) => ({ - ...parsed, - [key]: parsePattern(pattern[key]) - }), - {} + return Object.fromEntries( + Object.keys(pattern).map((key) => [key, parsePattern(pattern[key])]) ); } const { value, regex, confidence, version } = String(pattern) - .split('\\;') + .split(String.raw`\;`) .reduce((attrs, attr, i) => { if (i) { const parts = attr.split(':'); @@ -51,11 +47,11 @@ function parsePattern(pattern, isRegex = true) { attrs.regex = new RegExp( isRegex ? attr - .replace(/\//g, '\\/') - .replace(/\\\+/g, '__escapedPlus__') - .replace(/\+/g, '{1,250}') - .replace(/\*/g, '{0,250}') - .replace(/__escapedPlus__/g, '\\+') + .replaceAll('/', String.raw`\/`) + .replaceAll(String.raw`\+`, '__escapedPlus__') + .replaceAll('+', '{1,250}') + .replaceAll('*', '{0,250}') + .replaceAll('__escapedPlus__', String.raw`\+`) : '', 'i' ); @@ -66,7 +62,7 @@ function parsePattern(pattern, isRegex = true) { return { value, regex, - confidence: parseInt(confidence || 100, 10), + confidence: Number.parseInt(confidence || 100, 10), version: version || '' }; } @@ -98,8 +94,8 @@ function resolveVersion({ version, regex }, match) { if (!matches) return version; let resolved = version; - matches.forEach((m, index) => { - if (String(m).length > 10) return; + for (const [index, m] of matches.entries()) { + if (String(m).length > 10) continue; const ternary = new RegExp(`\\\\${index}\\?([^:]+):(.*)$`).exec(version); if (ternary && ternary.length === 3) { @@ -108,8 +104,8 @@ function resolveVersion({ version, regex }, match) { resolved = resolved .trim() - .replace(new RegExp(`\\\\${index}`, 'g'), m || ''); - }); + .replaceAll(new RegExp(`\\\\${index}`, 'g'), m || ''); + } return resolved.replace(/\\\d/, ''); } @@ -117,8 +113,8 @@ function resolveVersion({ version, regex }, match) { function getTechnology(name) { return [ ...state.technologies, - ...state.requires.map(({ technologies }) => technologies).flat(), - ...state.categoryRequires.map(({ technologies }) => technologies).flat() + ...state.requires.flatMap(({ technologies }) => technologies), + ...state.categoryRequires.flatMap(({ technologies }) => technologies) ].find(({ name: _name }) => name === _name); } @@ -126,6 +122,13 @@ function getCategory(id) { return state.categories.find(({ id: _id }) => id === _id); } +function categoryPriority({ technology: { categories } }) { + return categories.reduce( + (max, id) => Math.max(max, getCategory(id).priority), + 0 + ); +} + export function setTechnologies(data) { state.technologies = Object.keys(data).reduce((technologies, name) => { const { @@ -167,9 +170,8 @@ export function setTechnologies(data) { dns: transformPatterns(dns), dom: transformPatterns( typeof dom === 'string' || Array.isArray(dom) - ? toArray(dom).reduce( - (acc, selector) => ({ ...acc, [selector]: { exists: '' } }), - {} + ? Object.fromEntries( + toArray(dom).map((selector) => [selector, { exists: '' }]) ) : dom, true, @@ -215,37 +217,37 @@ export function setTechnologies(data) { }, []); const requiresMap = {}; - state.technologies - .filter(({ requires }) => requires.length) - .forEach((technology) => - technology.requires.forEach(({ name }) => { - if (!getTechnology(name)) { - throw new Error(`Required technology does not exist: ${name}`); - } - (requiresMap[name] ||= []).push(technology); - }) - ); + for (const technology of state.technologies.filter( + ({ requires }) => requires.length + )) + for (const { name } of technology.requires) { + if (!getTechnology(name)) { + throw new Error(`Required technology does not exist: ${name}`); + } + (requiresMap[name] ||= []).push(technology); + } + state.requires = Object.keys(requiresMap).map((name) => ({ name, technologies: requiresMap[name] })); const categoryRequiresMap = {}; - state.technologies - .filter(({ requiresCategory }) => requiresCategory.length) - .forEach((technology) => - technology.requiresCategory.forEach(({ id }) => { - (categoryRequiresMap[id] ||= []).push(technology); - }) - ); + for (const technology of state.technologies.filter( + ({ requiresCategory }) => requiresCategory.length + )) + for (const { id } of technology.requiresCategory) { + (categoryRequiresMap[id] ||= []).push(technology); + } + state.categoryRequires = Object.keys(categoryRequiresMap).map((id) => ({ - categoryId: parseInt(id, 10), + categoryId: Number.parseInt(id, 10), technologies: categoryRequiresMap[id] })); state.technologies = state.technologies.filter( ({ requires, requiresCategory }) => - !requires.length && !requiresCategory.length + requires.length === 0 && requiresCategory.length === 0 ); } @@ -254,7 +256,7 @@ export function setCategories(data) { .reduce((categories, id) => { const category = data[id]; categories.push({ - id: parseInt(id, 10), + id: Number.parseInt(id, 10), slug: slugify(category.name), ...category }); @@ -265,7 +267,7 @@ export function setCategories(data) { function analyzeOneToOne(technology, type, value) { return technology[type].reduce((technologies, pattern) => { - if (pattern.regex.exec(value)) { + if (pattern.regex.test(value)) { technologies.push({ technology, pattern: { @@ -283,7 +285,7 @@ function analyzeOneToOne(technology, type, value) { function analyzeOneToMany(technology, type, items = []) { return items.reduce((technologies, value) => { - (technology[type] || []).forEach((pattern) => { + for (const pattern of technology[type] || []) { const matches = pattern.regex.exec(value); if (matches) { technologies.push({ @@ -292,7 +294,7 @@ function analyzeOneToMany(technology, type, items = []) { version: resolveVersion(pattern, value) }); } - }); + } return technologies; }, []); } @@ -302,12 +304,12 @@ function analyzeManyToMany(technology, types, items = {}) { return Object.keys(technology[type]).reduce((technologies, key) => { const patterns = technology[type][key] || []; const values = items[key] || []; - patterns.forEach((_pattern) => { + for (const _pattern of patterns) { const pattern = (subtypes || []).reduce( (p, subtype) => p[subtype] || {}, _pattern ); - values.forEach((value) => { + for (const value of values) { const matches = pattern.regex.exec(value); if (matches) { technologies.push({ @@ -316,8 +318,8 @@ function analyzeManyToMany(technology, types, items = {}) { version: resolveVersion(pattern, value) }); } - }); - }); + } + } return technologies; }, []); } @@ -346,24 +348,21 @@ export function analyze(items, technologies = state.technologies) { try { return technologies - .map((technology) => - Object.keys(relations) - .map( - (type) => - items[type] && relations[type](technology, type, items[type]) - ) - .flat() + .flatMap((technology) => + Object.keys(relations).flatMap( + (type) => + items[type] && relations[type](technology, type, items[type]) + ) ) - .flat() - .filter((t) => t); + .filter(Boolean); } catch (error) { throw new Error(error.message || error.toString(), { cause: error }); } } function resolveExcludes(resolved) { - resolved.forEach(({ technology }) => { - technology.excludes.forEach(({ name }) => { + for (const { technology } of resolved) { + for (const { name } of technology.excludes) { const excluded = getTechnology(name); if (!excluded) { throw new Error(`Excluded technology does not exist: ${name}`); @@ -375,66 +374,65 @@ function resolveExcludes(resolved) { ); if (index !== -1) resolved.splice(index, 1); } while (index !== -1); - }); - }); + } + } } function resolveImplies(resolved) { let done; do { done = true; - resolved.forEach(({ technology, confidence, lastUrl }) => { - technology.implies.forEach( - ({ name, confidence: _confidence, version }) => { - const implied = getTechnology(name); - if (!implied) { - throw new Error(`Implied technology does not exist: ${name}`); - } - if ( - resolved.findIndex( - ({ technology: { name: n } }) => n === implied.name - ) === -1 - ) { - resolved.push({ - technology: implied, - confidence: Math.min(confidence, _confidence), - version: version || '', - lastUrl - }); - done = false; - } + for (const { technology, confidence, lastUrl } of resolved) { + for (const { + name, + confidence: _confidence, + version + } of technology.implies) { + const implied = getTechnology(name); + if (!implied) { + throw new Error(`Implied technology does not exist: ${name}`); } - ); - }); - } while (resolved.length && !done); + if ( + !resolved.some(({ technology: { name: n } }) => n === implied.name) + ) { + resolved.push({ + technology: implied, + confidence: Math.min(confidence, _confidence), + version: version || '', + lastUrl + }); + done = false; + } + } + } + } while (resolved.length > 0 && !done); } export function resolve(detections = []) { const resolved = detections.reduce((acc, { technology, lastUrl }) => { - if ( - acc.findIndex(({ technology: { name } }) => name === technology.name) !== - -1 - ) { + if (acc.some(({ technology: { name } }) => name === technology.name)) { return acc; } let version = ''; let confidence = 0; let rootPath; - detections - .filter( - ({ technology: _technology }) => - _technology && _technology.name === technology.name - ) - .forEach(({ pattern, version: _version = '', rootPath: _rootPath }) => { - confidence = Math.min(100, confidence + pattern.confidence); - version = - _version.length > version.length && - _version.length <= 15 && - (parseInt(_version, 10) || 0) < 10000 - ? _version - : version; - rootPath = rootPath || _rootPath || undefined; - }); + for (const { + pattern, + version: _version = '', + rootPath: _rootPath + } of detections.filter( + ({ technology: _technology }) => + _technology && _technology.name === technology.name + )) { + confidence = Math.min(100, confidence + pattern.confidence); + version = + _version.length > version.length && + _version.length <= 15 && + (Number.parseInt(_version, 10) || 0) < 10_000 + ? _version + : version; + rootPath = rootPath || _rootPath || undefined; + } acc.push({ technology, confidence, version, rootPath, lastUrl }); return acc; }, []); @@ -442,11 +440,8 @@ export function resolve(detections = []) { resolveExcludes(resolved); resolveImplies(resolved); - const priority = ({ technology: { categories } }) => - categories.reduce((max, id) => Math.max(max, getCategory(id).priority), 0); - return resolved - .sort((a, b) => (priority(a) > priority(b) ? 1 : -1)) + .sort((a, b) => (categoryPriority(a) > categoryPriority(b) ? 1 : -1)) .map( ({ technology: { diff --git a/package-lock.json b/package-lock.json index 44f06de..265dd5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "eslint": "10.4.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.5", + "eslint-plugin-unicorn": "64.0.0", "globals": "17.6.0", "prettier": "^3.4.2", "terser": "^5.37.0" @@ -27,6 +28,16 @@ "node": ">=20.0.0" } }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bazel/runfiles": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz", @@ -1818,6 +1829,19 @@ "node": "18 || 20 || >=22" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", + "integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -1876,6 +1900,41 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/browsertime": { "version": "27.0.0", "resolved": "https://registry.npmjs.org/browsertime/-/browsertime-27.0.0.tgz", @@ -1978,6 +2037,19 @@ "dev": true, "license": "MIT" }, + "node_modules/builtin-modules": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.2.0.tgz", + "integrity": "sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -2002,6 +2074,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/cbor": { "version": "10.0.12", "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.12.tgz", @@ -2028,6 +2121,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -2103,6 +2203,29 @@ "dev": true, "license": "MIT" }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/cli-truncate": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", @@ -2333,6 +2456,20 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2431,6 +2568,13 @@ "dev": true, "license": "MIT" }, + "node_modules/electron-to-chromium": { + "version": "1.5.357", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz", + "integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==", + "dev": true, + "license": "ISC" + }, "node_modules/emittery": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-1.2.1.tgz", @@ -2579,6 +2723,40 @@ } } }, + "node_modules/eslint-plugin-unicorn": { + "version": "64.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-64.0.0.tgz", + "integrity": "sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "@eslint-community/eslint-utils": "^4.9.1", + "change-case": "^5.4.4", + "ci-info": "^4.4.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.49.0", + "find-up-simple": "^1.0.1", + "globals": "^17.4.0", + "indent-string": "^5.0.0", + "is-builtin-module": "^5.0.0", + "jsesc": "^3.1.0", + "pluralize": "^8.0.0", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.13.0", + "semver": "^7.7.4", + "strip-indent": "^4.1.1" + }, + "engines": { + "node": "^20.10.0 || >=21.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=9.38.0" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -3293,6 +3471,22 @@ "node": ">=8" } }, + "node_modules/is-builtin-module": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^5.0.0" + }, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3497,6 +3691,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3918,6 +4125,13 @@ "node": ">=10.16" } }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-stream-zip": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", @@ -4221,6 +4435,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -4293,6 +4514,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/pngjs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", @@ -4425,6 +4656,29 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4981,6 +5235,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strtok3": { "version": "10.3.5", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", @@ -5251,6 +5518,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index 9ce052a..bcf3b0e 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "eslint": "10.4.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.5", + "eslint-plugin-unicorn": "64.0.0", "globals": "17.6.0", "prettier": "^3.4.2", "terser": "^5.37.0"