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 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
+ ![License](https://img.shields.io/github/license/yuschick/stylelint-plugin-defensive-css?style=for-the-badge)
4
+ ![NPM Version](https://img.shields.io/npm/v/stylelint-plugin-defensive-css?style=for-the-badge)
5
+ ![Main Workflow Status](https://img.shields.io/github/actions/workflow/status/yuschick/stylelint-plugin-defensive-css/main.yaml?style=for-the-badge)
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,3 @@
1
+ const useDefensiveCSS = require('./rules/use-defensive-css');
2
+
3
+ module.exports = [useDefensiveCSS];
@@ -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);
@@ -0,0 +1,9 @@
1
+ const expression = /\b(repeat|repeat-x|repeat-y|space|round|no-repeat|)\b/g;
2
+
3
+ function findShorthandBackgroundRepeat(value) {
4
+ return value.match(expression).some((val) => val);
5
+ }
6
+
7
+ module.exports = {
8
+ findShorthandBackgroundRepeat,
9
+ };
@@ -0,0 +1,10 @@
1
+ const expression = /-[-moz-|-ms-|-o-|-webkit-|]+-/g;
2
+
3
+ function findVendorPrefixes(selector) {
4
+ let prefixesFound = [...selector.matchAll(expression)];
5
+ return prefixesFound.length > 1;
6
+ }
7
+
8
+ module.exports = {
9
+ findVendorPrefixes,
10
+ };