From 759ad8a41a6c7b9a438c5dbbeed3487f45ddd219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20=C3=96sterberg?= Date: Fri, 12 Jun 2026 16:14:40 +0200 Subject: [PATCH 1/2] Refetch CSS bodies missing from the HAR Custom properties are resolved by stylelint's no-unknown-custom-properties rule only against definitions present in the linted CSS. When a stylesheet that defines custom properties is served from cache, returns 304 Not Modified, or is recorded with an empty body, its HAR entry has no content and was silently dropped. Every var() referencing those properties in the stylesheets that *were* captured was then falsely reported as "Unknown custom property". Detect bodyless CSS entries (by mime type or .css URL) and refetch their content over HTTP before linting. Failures (network error, timeout, non-OK, empty body) are ignored so a single unreachable file never fails the test. --- lib/harAnalyzer.js | 57 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) 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