Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,55 @@ For the above setup you could use the following config:
Last 2 versions
```

## New in v8.0.4-modified.0: `ignoreWithinAtSupports` Option

- `ignoreWithinAtSupports`: optional, off by default. Accepts a boolean. When enabled:
- Skips browser compatibility warnings for features wrapped in valid `@supports` rules
- Respects nested conditions and logical operators (`and`/`or`/`not`)
- Still validates features outside `@supports` or in non-matching conditions

### Usage Examples

```json
{
"plugins": ["stylelint-no-unsupported-browser-features"],
"rules": {
"plugin/no-unsupported-browser-features": [
true,
{
"browsers": ["IE 11"],
"ignoreWithinAtSupports": true
}
]
}
}
```

### Behavior Examples

✅ **Allowed**:
```css
@supports (display: grid) {
.container { display: grid; } /* No warning */
}
```

❌ **Still Reported**:
```css
/* Without @supports */
.container { display: grid; } /* Warning */

/* Non-matching condition */
@supports (display: flex) {
.container { display: grid; } /* Warning */
}

/* Uncertain conditions (OR) */
@supports (display: grid) or (display: flex) {
.container { display: grid; } /* Warning */
}
```

## Known issues

- [Visual Studio Code](https://code.visualstudio.com) users leveraging stylelint-no-unsupported-browser-features through the official [stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) extension will need to restart VSCode after making changes to their browserslist configuration file. It seems that either VSCode or the extension are causing browserlist config files to be cached and are not watching for changes in the file. If you are relying on the `browsers` property within the rules section of `.stylelintrc` you can ignore this issue. Changes to the `browsers` property are discovered immediately.
Expand Down
52 changes: 50 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import stylelint from 'stylelint';
import doiuse from 'doiuse';
import { Result } from 'postcss';
import { SupportsParser } from 'css-supports-parser';

/**
* Plugin settings
Expand All @@ -11,6 +12,11 @@ const messages = stylelint.utils.ruleMessages(ruleName, {
rejected: (details) => `Unexpected browser feature ${details}`,
});

/**
* @type {Map<string, SupportsParser>}
*/
const supportsMap = new Map();
Comment on lines +15 to +18
Copy link

Copilot AI May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global supportsMap is never cleared and could grow without bound across lint runs; consider scoping or clearing it to prevent a memory leak.

Suggested change
/**
* @type {Map<string, SupportsParser>}
*/
const supportsMap = new Map();
// Removed global declaration of supportsMap

Copilot uses AI. Check for mistakes.

/**
* Options
*/
Expand All @@ -27,6 +33,7 @@ const optionsSchema = {
browsers: [isString],
ignore: [isString],
ignorePartialSupport: isBoolean,
ignoreWithinAtSupports: isBoolean,
};

/**
Expand Down Expand Up @@ -62,6 +69,41 @@ function cleanWarningText(warningText, ignorePartialSupport) {
return cleanedWarningText;
}

/**
* Check if node is inside @supports and feature is in positive condition
*/
function isFeatureInPositiveSupportsCondition(node) {
// Skip if feature is as @supports condition
if (node.type === 'atrule' && node.name === 'supports') {
return true;
}

let result = false;
let parent = node.parent;

while (parent) {
if (parent.type === 'atrule' && parent.name === 'supports') {
if (!supportsMap.has(parent.params)) {
supportsMap.set(parent.params, new SupportsParser(parent.params, true));
}

const parser = supportsMap.get(parent.params);

switch (parser.checkProperty(node.toString())) {
Copy link

Copilot AI May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle the case where checkProperty returns undefined (neither true nor false) to avoid treating unsupported conditions as supported; consider adding a default case to the switch.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Rather than passing node.toString(), use the declaration's node.prop (e.g., 'display') to feed the parser only the property name for better accuracy and performance.

Suggested change
switch (parser.checkProperty(node.toString())) {
switch (parser.checkProperty(node.prop)) {

Copilot uses AI. Check for mistakes.
case true:
result = true;
break;

case false:
return false;
}
}
parent = parent.parent;
}

return result;
}

/**
* The main plugin rule
*/
Expand Down Expand Up @@ -93,15 +135,21 @@ function ruleFunction(on, options) {
usedFeatures[info.usage] = info.featureData;
};

const { ignorePartialSupport } = doiuseOptions;
const { ignorePartialSupport, ignoreWithinAtSupports } = doiuseOptions;

doiuse(doiuseOptions).postcss(root, doiuseResult);
doiuseResult.warnings().forEach((doiuseWarning) => {
const featureData = usedFeatures[doiuseWarning.node];
const node = doiuseWarning.node;
const featureData = usedFeatures[node];
if (featureData && ignorePartialSupport && featureData.partial && !featureData.missing) {
return;
}

// Skip if feature is inside @supports and option is enabled
if (ignoreWithinAtSupports && isFeatureInPositiveSupportsCondition(node)) {
return;
}

stylelint.utils.report({
ruleName,
result,
Expand Down
181 changes: 181 additions & 0 deletions lib/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,184 @@ testRule({
},
],
});

/**
* ignoreWithinAtSupports option
*/

// Basic @supports acceptance
testRule({
plugins: ['./lib'],
ruleName,
config: [
true,
{
browsers: ['IE 11'],
ignore: ['css-featurequeries'],
ignoreWithinAtSupports: true,
},
],

accept: [
{
code: '@supports (display: flex) { div { display: flex; } }',
description: 'should ignore unsupported features inside matching @supports',
},
{
code: '@supports (not (display: grid)) { div { display: table; } }',
description: 'should ignore when inside negated @supports that matches',
},
{
code: `
@supports (display: table) {
@supports (display: flex) {
div { display: flex; }
}
}`,
description: 'should handle nested @supports blocks',
},
],

reject: [
{
code: '@supports (not (display: flex)) { div { display: flex; } }',
description: 'should still reject unsupported features outside @supports',
message: `Unexpected browser feature "flexbox" is only partially supported by IE 11 (plugin/no-unsupported-browser-features)`,
line: 1,
column: 41,
},
{
code: '@supports (display: grid) { div { display: flex; } }',
description: 'should reject when @supports condition doesnt match',
message: `Unexpected browser feature "flexbox" is only partially supported by IE 11 (plugin/no-unsupported-browser-features)`,
line: 1,
column: 35,
},
],
});

// Complex logical operators
testRule({
plugins: ['./lib'],
ruleName,
config: [
true,
{
browsers: ['IE 7'],
ignore: ['css-featurequeries'],
ignoreWithinAtSupports: true,
},
],

reject: [
{
code: '@supports (display: flex) or (display: grid) { div { display: flex; } }',
description: 'should reject with OR conditions (uncertain support)',
message: `Unexpected browser feature "flexbox" is not supported by IE 7 (plugin/no-unsupported-browser-features)`,
line: 1,
column: 54,
},
{
code: '@supports (display: flex) and (display: grid) { div { display: table; } }',
description: 'should reject when one condition in AND is unsupported',
message: `Unexpected browser feature "css-table" is not supported by IE 7 (plugin/no-unsupported-browser-features)`,
line: 1,
column: 55,
},
],

accept: [
{
code: '@supports (display: table) and (not (display: flex)) { div { display: table; } }',
description: 'should accept when all AND conditions are satisfied',
},
],
});

// Edge cases with partial support
testRule({
plugins: ['./lib'],
ruleName,
config: [
true,
{
browsers: ['IE 9'],
ignore: ['css-featurequeries'],
ignoreWithinAtSupports: true,
},
],

accept: [
{
code: '@supports (width: 1rem) { div { width: 1rem; } }',
description:
'should acceot partial support when ignorePartialSupport=false while declared as condition',
Copy link

Copilot AI May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix typo in test description: change "acceot" to "accept".

Suggested change
'should acceot partial support when ignorePartialSupport=false while declared as condition',
'should accept partial support when ignorePartialSupport=false while declared as condition',

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update description to reference ignoreWithinAtSupports rather than ignorePartialSupport to match the test's intent.

Suggested change
'should acceot partial support when ignorePartialSupport=false while declared as condition',
'should accept partial support when ignoreWithinAtSupports=false while declared as condition',

Copilot uses AI. Check for mistakes.
},
],
});

// Combination with ignore option
testRule({
plugins: ['./lib'],
ruleName,
config: [
true,
{
browsers: ['IE 10'],
ignoreWithinAtSupports: true,
ignore: ['css-table', 'css-featurequeries'],
},
],

accept: [
{
code: 'div { display: table; }',
description: 'should respect ignore list outside @supports',
},
{
code: '@supports (display: flex) { div { display: flex; } }',
description: 'should respect ignoreWithinAtSupports inside @supports',
},
],
});

// Complex nested cases
testRule({
plugins: ['./lib'],
ruleName,
config: [
true,
{
browsers: ['IE 11'],
ignore: ['css-featurequeries'],
ignoreWithinAtSupports: true,
},
],

accept: [
{
code: `
@supports (display: grid) {
.grid { display: grid; }
@supports (not (display: flex)) {
.no-flex { display: grid; }
}
}`,
description: 'should handle complex nested @supports structures',
},
],

reject: [
{
code: `
@supports (display: grid) {
.grid { display: grid; }
.outside { display: flex; }
}`,
description: 'should reject rules outside @supports but inside its block',
message: `Unexpected browser feature "flexbox" is only partially supported by IE 11 (plugin/no-unsupported-browser-features)`,
line: 4,
column: 22,
},
],
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stylelint-no-unsupported-browser-features",
"version": "8.0.4",
"version": "8.0.4-modified.0",
"description": "Disallow features that are unsupported by the browsers that you are targeting",
"homepage": "https://github.com/RJWadley/stylelint-no-unsupported-browser-features#readme",
"scripts": {
Expand All @@ -17,6 +17,7 @@
"stylelint": "^16.0.2"
},
"dependencies": {
"css-supports-parser": "^1.0.0",
"doiuse": "^6.0.5",
"postcss": "^8.4.32"
},
Expand Down
Loading