diff --git a/eslint.config.js b/eslint.config.js index 9dbc538..777a87c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,11 @@ export default [ globals: { node: true, es6: true, - URL: "readonly" + URL: "readonly", + fetch: "readonly", + AbortController: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly" }, parserOptions: { ecmaVersion: 'latest', diff --git a/lib/harAnalyzer.js b/lib/harAnalyzer.js index 089ef3f..6bf1784 100644 --- a/lib/harAnalyzer.js +++ b/lib/harAnalyzer.js @@ -20,7 +20,7 @@ export class HarAnalyzer { this.version = this.package.version; } - transform2SimplifiedData(harData, url) { + async transform2SimplifiedData(harData, url) { const data = { 'url': url, 'rules': this.config.rules, @@ -37,12 +37,25 @@ export class HarAnalyzer { let reqIndex = 1; + // CSS resources whose response body is missing from the HAR (served from + // cache, 304 Not Modified, or recorded with an empty body). We refetch + // these below — otherwise the custom properties they define are absent + // from the linted CSS and every var() referencing them elsewhere is + // falsely reported as "Unknown custom property" (no-unknown-custom-properties). + const missingCssUrls = new Set(); + for (const entry of harData.entries) { const req = entry.request; const res = entry.response; const reqUrl = req.url; + const mimeType = (res && res.content && res.content.mimeType) || ''; + const looksLikeCss = mimeType.includes('css') || /\.css(\?|#|$)/i.test(reqUrl); + if (!res.content || !res.content.text || !res.content.mimeType || !res.content.size || res.content.size <= 0 || !res.status) { + if (looksLikeCss && /^https?:\/\//i.test(reqUrl)) { + missingCssUrls.add(reqUrl); + } continue; } @@ -62,6 +75,46 @@ export class HarAnalyzer { reqIndex++; } + // Refetch the bodies of CSS files that were absent from the HAR, skipping + // any we already captured. Failures (network error, timeout, non-OK) are + // ignored so a single unreachable file never fails the whole CSS test. + const refetchUrls = [...missingCssUrls].filter( + (cssUrl) => !data['style-files'].some((o) => o.url === cssUrl) + ); + const refetched = await Promise.all( + refetchUrls.map(async (cssUrl) => { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); + const response = await fetch(cssUrl, { signal: controller.signal }); + clearTimeout(timeout); + if (!response.ok) { + return null; + } + const text = await response.text(); + if (!text) { + return null; + } + return { url: cssUrl, content: text }; + } catch { + return null; + } + }) + ); + for (const item of refetched) { + if (!item) { + continue; + } + const obj = { + 'url': item.url, + 'content': item.content, + 'index': reqIndex + }; + data['all-styles'].push(obj); + data['style-files'].push(obj); + reqIndex++; + } + // Extract