From ad9ddd769b38ba26f09c1a76c0da80d5144dff53 Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Thu, 20 Feb 2025 02:15:42 +0900 Subject: [PATCH] feat: Add support for media conditions in require-baseline rule (#49) * feat: Add support for media conditions in require-baseline rule * Add check for invalid at-rules * test: ignore unknown media conditions --- docs/rules/require-baseline.md | 8 +++ src/data/baseline-data.js | 40 ++++++++++++++ src/rules/require-baseline.js | 47 ++++++++++++++++ tests/rules/require-baseline.test.js | 80 +++++++++++++++++++++++++++- tools/generate-baseline.js | 11 ++++ 5 files changed, 185 insertions(+), 1 deletion(-) diff --git a/docs/rules/require-baseline.md b/docs/rules/require-baseline.md index cfeaca7..1f5adcb 100644 --- a/docs/rules/require-baseline.md +++ b/docs/rules/require-baseline.md @@ -20,6 +20,7 @@ This rule warns when it finds any of the following: - A CSS property that isn't widely available or otherwise isn't enclosed in a `@supports` block. - An at-rule that isn't widely available. +- A media condition inside `@media` that isn't widely available. - A CSS property value that isn't widely available or otherwise isn't enclosed in a `@supports` block (currently limited to identifiers only). - A CSS property function that isn't widely available. @@ -38,6 +39,13 @@ a { width: abs(20% - 100px); } +/* invalid - device-posture is not widely available */ +@media (device-posture: folded) { + a { + color: red; + } +} + /* invalid - property value doesn't match @supports indicator */ @supports (accent-color: auto) { a { diff --git a/src/data/baseline-data.js b/src/data/baseline-data.js index c57f7ff..6a43751 100644 --- a/src/data/baseline-data.js +++ b/src/data/baseline-data.js @@ -517,6 +517,46 @@ export const atRules = new Map([ ["starting-style", 0], ["supports", 10], ]); +export const mediaConditions = new Map([ + ["color-gamut", 5], + ["device-posture", 0], + ["device-aspect-ratio", 0], + ["device-height", 0], + ["device-width", 0], + ["display-mode", 0], + ["dynamic-range", 10], + ["forced-colors", 5], + ["any-hover", 10], + ["any-pointer", 10], + ["hover", 10], + ["pointer", 10], + ["inverted-colors", 0], + ["aspect-ratio", 10], + ["calc", 10], + ["color", 10], + ["color-index", 10], + ["grid", 10], + ["height", 10], + ["monochrome", 10], + ["nested-queries", 10], + ["orientation", 10], + ["width", 10], + ["overflow-block", 5], + ["overflow-inline", 5], + ["prefers-color-scheme", 10], + ["prefers-contrast", 10], + ["prefers-reduced-data", 0], + ["prefers-reduced-motion", 10], + ["prefers-reduced-transparency", 0], + ["resolution", 5], + ["-webkit-device-pixel-ratio", 10], + ["-webkit-max-device-pixel-ratio", 10], + ["-webkit-min-device-pixel-ratio", 10], + ["scripting", 5], + ["-webkit-transform-3d", 10], + ["update", 5], + ["video-dynamic-range", 0], +]); export const types = new Map([ ["abs", 0], ["sign", 0], diff --git a/src/rules/require-baseline.js b/src/rules/require-baseline.js index 6e5094c..6b9d90b 100644 --- a/src/rules/require-baseline.js +++ b/src/rules/require-baseline.js @@ -13,6 +13,7 @@ import { properties, propertyValues, atRules, + mediaConditions, types, } from "../data/baseline-data.js"; import { namedColors } from "../data/colors.js"; @@ -344,6 +345,8 @@ export default { "At-rule '@{{atRule}}' is not a {{availability}} available baseline feature.", notBaselineType: "Type '{{type}}' is not a {{availability}} available baseline feature.", + notBaselineMediaCondition: + "Media condition '{{condition}}' is not a {{availability}} available baseline feature.", }, }, @@ -549,6 +552,50 @@ export default { supportsRules.pop(); }, + "Atrule[name=media] > AtrulePrelude > MediaQueryList > MediaQuery > Condition"( + node, + ) { + for (const child of node.children) { + // ignore unknown media conditions - no-invalid-at-rules already catches this + if (!mediaConditions.has(child.name)) { + continue; + } + + if (child.type !== "Feature") { + continue; + } + + const conditionLevel = mediaConditions.get(child.name); + + if (conditionLevel < baselineLevel) { + const loc = child.loc; + + context.report({ + loc: { + start: { + line: loc.start.line, + // add 1 to account for the @ symbol + column: loc.start.column + 1, + }, + end: { + line: loc.start.line, + column: + // add 1 to account for the @ symbol + loc.start.column + + child.name.length + + 1, + }, + }, + messageId: "notBaselineMediaCondition", + data: { + condition: child.name, + availability, + }, + }); + } + } + }, + Atrule(node) { // ignore unknown at-rules - no-invalid-at-rules already catches this if (!atRules.has(node.name)) { diff --git a/tests/rules/require-baseline.test.js b/tests/rules/require-baseline.test.js index a8ac83b..2147367 100644 --- a/tests/rules/require-baseline.test.js +++ b/tests/rules/require-baseline.test.js @@ -34,6 +34,8 @@ ruleTester.run("require-baseline", rule, { "a { color: red; -moz-transition: bar }", "@font-face { font-weight: 100 400 }", "@media (min-width: 800px) { a { color: red; } }", + "@media (foo) { a { color: red; } }", + "@media (prefers-color-scheme: dark) { a { color: red; } }", "@supports (accent-color: auto) { a { accent-color: auto; } }", "@supports (accent-color: red) { a { accent-color: red; } }", "@supports (accent-color: auto) { a { accent-color: red; } }", @@ -180,7 +182,7 @@ ruleTester.run("require-baseline", rule, { @supports (backdrop-filter: auto) { a { accent-color: red; } } - + a { backdrop-filter: auto; } }`, errors: [ @@ -262,5 +264,81 @@ ruleTester.run("require-baseline", rule, { }, ], }, + { + code: "@media (color-gamut: srgb) { a { color: red; } }", + errors: [ + { + messageId: "notBaselineMediaCondition", + data: { + condition: "color-gamut", + availability: "widely", + }, + line: 1, + column: 9, + endLine: 1, + endColumn: 20, + }, + ], + }, + { + code: "@media (device-posture: folded) { a { color: red; } }", + options: [{ available: "newly" }], + errors: [ + { + messageId: "notBaselineMediaCondition", + data: { + condition: "device-posture", + availability: "newly", + }, + line: 1, + column: 9, + endLine: 1, + endColumn: 23, + }, + ], + }, + { + code: "@media (height: 600px) and (color-gamut: srgb) and (device-posture: folded) { a { color: red; } }", + errors: [ + { + messageId: "notBaselineMediaCondition", + data: { + condition: "color-gamut", + availability: "widely", + }, + line: 1, + column: 29, + endLine: 1, + endColumn: 40, + }, + { + messageId: "notBaselineMediaCondition", + data: { + condition: "device-posture", + availability: "widely", + }, + line: 1, + column: 53, + endLine: 1, + endColumn: 67, + }, + ], + }, + { + code: "@media (foo) and (color-gamut: srgb) { a { color: red; } }", + errors: [ + { + messageId: "notBaselineMediaCondition", + data: { + condition: "color-gamut", + availability: "widely", + }, + line: 1, + column: 19, + endLine: 1, + endColumn: 30, + }, + ], + }, ], }); diff --git a/tools/generate-baseline.js b/tools/generate-baseline.js index 9aba6a5..e5b42bc 100644 --- a/tools/generate-baseline.js +++ b/tools/generate-baseline.js @@ -72,12 +72,15 @@ function extractCSSFeatures(features) { const cssPropertyValuePattern = /^css\.properties\.(?[a-zA-Z$\d-]+)\.(?[a-zA-Z$\d-]+)$/u; const cssAtRulePattern = /^css\.at-rules\.(?[a-zA-Z$\d-]+)$/u; + const cssMediaConditionPattern = + /^css\.at-rules\.media\.(?[a-zA-Z$\d-]+)$/u; const cssTypePattern = /^css\.types\.(?[a-zA-Z$\d-]+)$/u; const cssSelectorPattern = /^css\.selectors\.(?[a-zA-Z$\d-]+)$/u; const properties = {}; const propertyValues = {}; const atRules = {}; + const mediaConditions = {}; const types = {}; const selectors = {}; @@ -114,6 +117,12 @@ function extractCSSFeatures(features) { continue; } + // Media conditions (@media features) + if ((match = cssMediaConditionPattern.exec(key)) !== null) { + mediaConditions[match.groups.condition] = baselineIds.get(baseline); + continue; + } + // types if ((match = cssTypePattern.exec(key)) !== null) { types[match.groups.type] = baselineIds.get(baseline); @@ -131,6 +140,7 @@ function extractCSSFeatures(features) { properties, propertyValues, atRules, + mediaConditions, types, selectors, }; @@ -166,6 +176,7 @@ export const BASELINE_FALSE = ${BASELINE_FALSE}; export const properties = new Map(${JSON.stringify(Object.entries(cssFeatures.properties), null, "\t")}); export const atRules = new Map(${JSON.stringify(Object.entries(cssFeatures.atRules), null, "\t")}); +export const mediaConditions = new Map(${JSON.stringify(Object.entries(cssFeatures.mediaConditions), null, "\t")}); export const types = new Map(${JSON.stringify(Object.entries(cssFeatures.types), null, "\t")}); export const selectors = new Map(${JSON.stringify(Object.entries(cssFeatures.selectors), null, "\t")}); export const propertyValues = new Map([${Object.entries(