stylelint-plugin-defensive-css 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/package.json +56 -0
- package/src/index.js +3 -0
- package/src/rules/use-defensive-css/base.js +30 -0
- package/src/rules/use-defensive-css/index.js +134 -0
- package/src/utils/findShorthandBackgroundRepeat.js +9 -0
- package/src/utils/findVendorPrefixes.js +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Fedya Petrakov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# 🦖 Stylelint Plugin Defensive CSS
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
A Stylelint plugin to enforce defensive CSS best practices.
|
|
8
|
+
|
|
9
|
+
> [Read more about Defensive CSS](https://defensivecss.dev/)
|
|
10
|
+
|
|
11
|
+
## Getting Started
|
|
12
|
+
|
|
13
|
+
> Before getting started with the plugin, you must first have
|
|
14
|
+
> [Stylelint](https://stylelint.io/) version 14.0.0 or greater installed
|
|
15
|
+
|
|
16
|
+
To get started using the plugin, it must first be installed.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm i stylelint-plugin-defensive-css --save-dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
yarn add stylelint-plugin-defensive-css --dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
With the plugin installed, the rule(s) can be added to the project's Stylelint
|
|
27
|
+
configuration.
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"plugins": ["stylelint-plugin-defensive-css"],
|
|
32
|
+
"rules": {
|
|
33
|
+
"plugin/use-defensive-css": [
|
|
34
|
+
true,
|
|
35
|
+
{ "severity": "warning" }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Rules / Options
|
|
42
|
+
|
|
43
|
+
The plugin provides multiple rules that can be toggled on and off as needed.
|
|
44
|
+
|
|
45
|
+
### Background Repeat
|
|
46
|
+
|
|
47
|
+
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/bg-repeat/)
|
|
48
|
+
|
|
49
|
+
Oftentimes, when using a large image as a background, we tend to forget to account for the case when the design is viewed on a large screen. That background will repeat by default.
|
|
50
|
+
|
|
51
|
+
Enable this rule in order to prevent unintentional repeating background.
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"rules": {
|
|
56
|
+
"plugin/use-defensive-css": [
|
|
57
|
+
true,
|
|
58
|
+
{ ..., "background-repeat": true }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### ✅ Passing Examples
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
div {
|
|
68
|
+
background: url('some-image.jpg') repeat black top center;
|
|
69
|
+
}
|
|
70
|
+
div {
|
|
71
|
+
background: url('some-image.jpg') black top center;
|
|
72
|
+
background-repeat: no-repeat;
|
|
73
|
+
}
|
|
74
|
+
````
|
|
75
|
+
|
|
76
|
+
#### ❌ Failing Examples
|
|
77
|
+
|
|
78
|
+
```css
|
|
79
|
+
div {
|
|
80
|
+
background: url('some-image.jpg') black top center;
|
|
81
|
+
}
|
|
82
|
+
div {
|
|
83
|
+
background-image: url('some-image.jpg');
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
### Custom Property Fallbacks
|
|
89
|
+
|
|
90
|
+
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/css-variable-fallback/)
|
|
91
|
+
|
|
92
|
+
CSS variables are gaining more and more usage in web design. There is a method that we can apply to use them in a way that doesn’t break the experience, in case the CSS variable value was empty for some reason.
|
|
93
|
+
|
|
94
|
+
Enable this rule in order to require fallbacks values for custom properties.
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"rules": {
|
|
99
|
+
"plugin/use-defensive-css": [
|
|
100
|
+
true,
|
|
101
|
+
{ ..., "custom-property-fallbacks": true }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### ✅ Passing Examples
|
|
108
|
+
|
|
109
|
+
```css
|
|
110
|
+
div {
|
|
111
|
+
color: var(--color-primary, #000);
|
|
112
|
+
}
|
|
113
|
+
````
|
|
114
|
+
|
|
115
|
+
#### ❌ Failing Examples
|
|
116
|
+
|
|
117
|
+
```css
|
|
118
|
+
div {
|
|
119
|
+
color: var(--color-primary);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Flex Wrapping
|
|
124
|
+
|
|
125
|
+
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/flexbox-wrapping/)
|
|
126
|
+
|
|
127
|
+
CSS flexbox is one of the most useful CSS layout features nowadays. It’s tempting to add `display: flex` to a wrapper and have the child items ordered next to each other. The thing is when there is not enough space, those child items won’t wrap into a new line by default. We need to change that behavior with `flex-wrap: wrap`.
|
|
128
|
+
|
|
129
|
+
Enable this rule in order to require all flex rows to have a flex-wrap value.
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"rules": {
|
|
134
|
+
"plugin/use-defensive-css": [
|
|
135
|
+
true,
|
|
136
|
+
{ ..., "flex-wrapping": true }
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### ✅ Passing Examples
|
|
143
|
+
|
|
144
|
+
```css
|
|
145
|
+
div {
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-wrap: wrap;
|
|
148
|
+
}
|
|
149
|
+
div {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: row-reverse;
|
|
152
|
+
flex-wrap: wrap-reverse;
|
|
153
|
+
}
|
|
154
|
+
````
|
|
155
|
+
|
|
156
|
+
#### ❌ Failing Examples
|
|
157
|
+
|
|
158
|
+
```css
|
|
159
|
+
div {
|
|
160
|
+
display: flex;
|
|
161
|
+
}
|
|
162
|
+
div {
|
|
163
|
+
display: flex;
|
|
164
|
+
flex-direction: row;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Vendor Prefix Grouping
|
|
169
|
+
|
|
170
|
+
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/grouping-selectors/)
|
|
171
|
+
|
|
172
|
+
It's not recommended to group selectors that are meant to work with different browsers. For example, styling an input's placeholder needs multiple selectors per the browser. If we group the selectors, the entire rule will be invalid, according to [w3c](https://www.w3.org/TR/selectors/#grouping).
|
|
173
|
+
|
|
174
|
+
Enable this rule in order to require all vendor-prefixed selectors to be split into their own rules.
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"rules": {
|
|
179
|
+
"plugin/use-defensive-css": [
|
|
180
|
+
true,
|
|
181
|
+
{ ..., "vendor-prefix-grouping": true }
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
#### ✅ Passing Examples
|
|
189
|
+
|
|
190
|
+
```css
|
|
191
|
+
input::-webkit-input-placeholder {
|
|
192
|
+
color: #222;
|
|
193
|
+
}
|
|
194
|
+
input::-moz-placeholder {
|
|
195
|
+
color: #222;
|
|
196
|
+
}
|
|
197
|
+
````
|
|
198
|
+
|
|
199
|
+
#### ❌ Failing Examples
|
|
200
|
+
|
|
201
|
+
```css
|
|
202
|
+
input::-webkit-input-placeholder,
|
|
203
|
+
input::-moz-placeholder {
|
|
204
|
+
color: #222;
|
|
205
|
+
}
|
|
206
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stylelint-plugin-defensive-css",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A Stylelint plugin to enforce defensive CSS best practices.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/**/*.js",
|
|
8
|
+
"!**/**/*.test.js"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"prepare": "husky install",
|
|
12
|
+
"test": "jest"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/yuschick/stylelint-plugin-defensive-css.git"
|
|
17
|
+
},
|
|
18
|
+
"author": "Daniel Yuschick",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/yuschick/stylelint-plugin-defensive-css/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/yuschick/stylelint-plugin-defensive-css#readme",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=16.0.0"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"Stylelint",
|
|
29
|
+
"plugin",
|
|
30
|
+
"CSS",
|
|
31
|
+
"defensive CSS",
|
|
32
|
+
"best practices"
|
|
33
|
+
],
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"stylelint": "^14.0.0 || ^15.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@commitlint/cli": "^17.4.4",
|
|
39
|
+
"@commitlint/config-conventional": "^17.4.4",
|
|
40
|
+
"eslint": "^8.35.0",
|
|
41
|
+
"husky": "^8.0.0",
|
|
42
|
+
"jest": "^29.4.3",
|
|
43
|
+
"jest-cli": "^29.4.3",
|
|
44
|
+
"jest-preset-stylelint": "^6.1.0",
|
|
45
|
+
"lint-staged": "^13.1.2",
|
|
46
|
+
"prettier": "^2.8.4",
|
|
47
|
+
"prettier-eslint": "^15.0.1",
|
|
48
|
+
"stylelint": "^15.2.0"
|
|
49
|
+
},
|
|
50
|
+
"lint-staged": {
|
|
51
|
+
"**/*.{js|md}": [
|
|
52
|
+
"eslint",
|
|
53
|
+
"prettier --write"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const stylelint = require('stylelint');
|
|
4
|
+
|
|
5
|
+
const ruleName = 'plugin/use-defensive-css';
|
|
6
|
+
|
|
7
|
+
const ruleMessages = stylelint.utils.ruleMessages(ruleName, {
|
|
8
|
+
backgroundRepeat() {
|
|
9
|
+
return 'Ensure a background-repeat property is defined when using a background image.';
|
|
10
|
+
},
|
|
11
|
+
customPropertyFallbacks() {
|
|
12
|
+
return 'Ensure that any custom properties have a fallback value.';
|
|
13
|
+
},
|
|
14
|
+
flexWrapping() {
|
|
15
|
+
return 'Flex rows must have a `flex-wrap: wrap;` or `flex-wrap: wrap-reverse` declaration.';
|
|
16
|
+
},
|
|
17
|
+
vendorPrefixWGrouping() {
|
|
18
|
+
return `It's not recommended to group selectors that are meant to work with different browsers. Instead, split them to separate rules.`;
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const ruleMeta = {
|
|
23
|
+
url: 'https://github.com/yuschick/stylelint-plugin-defensive-css',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
ruleName,
|
|
28
|
+
ruleMessages,
|
|
29
|
+
ruleMeta,
|
|
30
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const stylelint = require('stylelint');
|
|
4
|
+
|
|
5
|
+
const { ruleName, ruleMessages, ruleMeta } = require('./base');
|
|
6
|
+
const {
|
|
7
|
+
findShorthandBackgroundRepeat,
|
|
8
|
+
} = require('../../utils/findShorthandBackgroundRepeat');
|
|
9
|
+
const { findVendorPrefixes } = require('../../utils/findVendorPrefixes');
|
|
10
|
+
|
|
11
|
+
const ruleFunction = (_, options) => {
|
|
12
|
+
let isLastStyleDeclaration = false;
|
|
13
|
+
const backgroundRepeatProps = {
|
|
14
|
+
hasBackgroundImage: false,
|
|
15
|
+
isMissingBackgroundRepeat: true,
|
|
16
|
+
nodeToReport: undefined,
|
|
17
|
+
};
|
|
18
|
+
const flexWrappingProps = {
|
|
19
|
+
isDisplayFlex: false,
|
|
20
|
+
isFlexRow: true,
|
|
21
|
+
isMissingFlexWrap: true,
|
|
22
|
+
nodeToReport: undefined,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (root, result) => {
|
|
26
|
+
const validOptions = stylelint.utils.validateOptions(result, ruleName);
|
|
27
|
+
|
|
28
|
+
if (!validOptions) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
root.walkDecls((decl) => {
|
|
33
|
+
if (
|
|
34
|
+
JSON.stringify(decl) ===
|
|
35
|
+
JSON.stringify(decl.parent.nodes[decl.parent.nodes.length - 1])
|
|
36
|
+
) {
|
|
37
|
+
isLastStyleDeclaration = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* BACKGROUND REPEAT */
|
|
41
|
+
if (options?.['background-repeat']) {
|
|
42
|
+
if (decl.prop === 'background' && decl.value.includes('url(')) {
|
|
43
|
+
backgroundRepeatProps.hasBackgroundImage = true;
|
|
44
|
+
backgroundRepeatProps.isMissingBackgroundRepeat =
|
|
45
|
+
!findShorthandBackgroundRepeat(decl.value);
|
|
46
|
+
backgroundRepeatProps.nodeToReport = decl;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (decl.prop === 'background-image' && decl.value.includes('url(')) {
|
|
50
|
+
backgroundRepeatProps.hasBackgroundImage = true;
|
|
51
|
+
backgroundRepeatProps.nodeToReport = decl;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (decl.prop === 'background-repeat') {
|
|
55
|
+
backgroundRepeatProps.isMissingBackgroundRepeat = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
isLastStyleDeclaration &&
|
|
60
|
+
Object.values(backgroundRepeatProps).every((prop) => prop)
|
|
61
|
+
) {
|
|
62
|
+
stylelint.utils.report({
|
|
63
|
+
message: ruleMessages.backgroundRepeat(),
|
|
64
|
+
node: decl.parent,
|
|
65
|
+
result,
|
|
66
|
+
ruleName,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* CUSTOM PROPERTY FALLBACKS */
|
|
72
|
+
if (options?.['custom-property-fallbacks']) {
|
|
73
|
+
if (decl.value.includes('var(--') && !decl.value.includes(',')) {
|
|
74
|
+
stylelint.utils.report({
|
|
75
|
+
message: ruleMessages.customPropertyFallbacks(),
|
|
76
|
+
node: decl,
|
|
77
|
+
result,
|
|
78
|
+
ruleName,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* FLEX WRAPPING */
|
|
84
|
+
if (options?.['flex-wrapping']) {
|
|
85
|
+
if (decl.prop === 'display' && decl.value.includes('flex')) {
|
|
86
|
+
flexWrappingProps.isDisplayFlex = true;
|
|
87
|
+
flexWrappingProps.nodeToReport = decl;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (decl.prop === 'flex-direction' && decl.value.startsWith('column')) {
|
|
91
|
+
flexWrappingProps.isFlexRow = false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (decl.prop === 'flex-wrap' && decl.value.startsWith('wrap')) {
|
|
95
|
+
flexWrappingProps.isMissingFlexWrap = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
isLastStyleDeclaration &&
|
|
100
|
+
Object.values(flexWrappingProps).every((prop) => prop)
|
|
101
|
+
) {
|
|
102
|
+
stylelint.utils.report({
|
|
103
|
+
message: ruleMessages.flexWrapping(),
|
|
104
|
+
node: flexWrappingProps.nodeToReport,
|
|
105
|
+
result,
|
|
106
|
+
ruleName,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* GROUPING VENDOR PREFIXES */
|
|
112
|
+
if (options?.['vendor-prefix-grouping']) {
|
|
113
|
+
const hasMultiplePrefixes = findVendorPrefixes(decl.parent.selector);
|
|
114
|
+
|
|
115
|
+
if (hasMultiplePrefixes) {
|
|
116
|
+
stylelint.utils.report({
|
|
117
|
+
message: ruleMessages.vendorPrefixWGrouping(),
|
|
118
|
+
node: decl.parent,
|
|
119
|
+
result,
|
|
120
|
+
ruleName,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return;
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
ruleFunction.ruleName = ruleName;
|
|
131
|
+
ruleFunction.messages = ruleMessages;
|
|
132
|
+
ruleFunction.meta = ruleMeta;
|
|
133
|
+
|
|
134
|
+
module.exports = stylelint.createPlugin(ruleName, ruleFunction);
|