@primer/stylelint-config 13.0.0-rc.51f6efe → 13.0.0-rc.54c7a2f

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/stylelint-config",
3
- "version": "13.0.0-rc.51f6efe",
3
+ "version": "13.0.0-rc.54c7a2f",
4
4
  "description": "Sharable stylelint config used by GitHub's CSS",
5
5
  "author": "GitHub, Inc.",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "@github/browserslist-config": "^1.0.0",
47
47
  "@primer/css": "^21.0.8",
48
- "@primer/primitives": "^7.17.1",
48
+ "@primer/primitives": "^9.0.1",
49
49
  "anymatch": "^3.1.1",
50
50
  "postcss-scss": "^4.0.2",
51
51
  "postcss-styled-syntax": "^0.6.4",
@@ -53,36 +53,31 @@
53
53
  "string.prototype.matchall": "^4.0.2",
54
54
  "stylelint": "^16.3.1",
55
55
  "stylelint-config-standard": "^36.0.0",
56
- "stylelint-css-modules-no-global-scoped-selector": "^1.0.2",
57
56
  "stylelint-no-unsupported-browser-features": "^8.0.0",
58
57
  "stylelint-order": "^6.0.4",
59
58
  "stylelint-scss": "^6.2.0",
60
59
  "stylelint-value-no-unknown-custom-properties": "^6.0.1",
61
60
  "tap-map": "^1.0.0"
62
61
  },
63
- "overrides": {
64
- "stylelint-css-modules-no-global-scoped-selector": {
65
- "stylelint": "$stylelint"
66
- }
67
- },
68
62
  "prettier": "@github/prettier-config",
69
63
  "devDependencies": {
70
64
  "@changesets/changelog-github": "^0.5.0",
71
- "@changesets/cli": "2.27.1",
65
+ "@changesets/cli": "2.27.7",
72
66
  "@github/prettier-config": "^0.0.6",
73
- "@rollup/plugin-commonjs": "^25.0.7",
67
+ "@rollup/plugin-commonjs": "^26.0.1",
74
68
  "@rollup/plugin-json": "^6.1.0",
75
69
  "@rollup/plugin-node-resolve": "^15.2.3",
76
- "@typescript-eslint/parser": "^7.7.0",
70
+ "@typescript-eslint/parser": "^8.0.1",
77
71
  "dedent": "^1.5.3",
78
- "eslint": "^8.0.1",
79
- "eslint-plugin-github": "^4.10.2",
72
+ "eslint": "^8.57.0",
73
+ "eslint-plugin-github": "^5.0.1",
74
+ "eslint-plugin-import": "^2.29.1",
80
75
  "eslint-plugin-jest": "^28.2.0",
81
76
  "eslint-plugin-prettier": "^5.1.3",
82
77
  "jest": "^29.7.0",
83
78
  "jest-preset-stylelint": "^7.0.0",
84
79
  "prettier": "^3.2.5",
85
- "rimraf": "^5.0.5",
80
+ "rimraf": "^6.0.1",
86
81
  "rollup": "^4.14.3"
87
82
  },
88
83
  "jest": {
package/plugins/README.md CHANGED
@@ -13,7 +13,6 @@ This directory contains all of our custom stylelint plugins, each of which provi
13
13
  - [`primer/borders`](#primerborders)
14
14
  - [`primer/box-shadow`](#primerbox-shadow)
15
15
  - [`primer/responsive-widths`](#primerresponsive-widths)
16
- - [`primer/utilities`](#primerutilities)
17
16
  - [Variable rules](#variable-rules)
18
17
  - [Variable rule options](#variable-rule-options)
19
18
 
@@ -84,31 +83,6 @@ This [variable rule](#variable-rules) enforces the use of `$box-shadow*` variabl
84
83
 
85
84
  This plugin checks for `width` and `min-width` declarations that use a value less than the minimum browser size. `320px`
86
85
 
87
- ## `primer/utilities`
88
-
89
- Checks for selectors with single declarations that can be replaced with [primer/css utilities](https://primer.style/css/utilities/).
90
-
91
- ```scss
92
- .foo {
93
- color: var(--color-fg-default);
94
- }
95
- /** ↑
96
- * FAIL: --color-fg-default can be replaced with .color-fg-default */
97
-
98
- .foo {
99
- color: #custom;
100
- }
101
- /** ↑
102
- * OK: Color value doesn't match a utility. */
103
-
104
- .foo {
105
- color: var(--color-fg-default);
106
- padding: 0;
107
- }
108
- /** ↑
109
- * OK: Has more than one declaration, not considered */
110
- ```
111
-
112
86
  ## Variable rules
113
87
 
114
88
  Variable rules are created using a general-purpose helper that can validate constraints for matching CSS properties and values. In general, the Primer CSS variable rules enforce two basic principles for custom CSS:
@@ -1,64 +1,197 @@
1
- import {createVariableRule} from './lib/variable-rules.js'
2
-
3
- export default createVariableRule(
4
- 'primer/borders',
5
- {
6
- border: {
7
- expects: 'a border variable',
8
- props: 'border{,-top,-right,-bottom,-left}',
9
- values: ['$border', 'none', '0'],
10
- components: ['border-width', 'border-style', 'border-color'],
11
- replacements: {
12
- // because shorthand border properties ¯\_(ツ)_/¯
13
- '$border-width $border-style $border-gray': '$border',
14
- '$border-width $border-gray $border-style': '$border',
15
- '$border-style $border-width $border-gray': '$border',
16
- '$border-style $border-gray $border-width': '$border',
17
- '$border-gray $border-width $border-style': '$border',
18
- '$border-gray $border-style $border-width': '$border',
19
- '$border-width $border-style $border-color': '$border',
20
- '$border-width $border-color $border-style': '$border',
21
- '$border-style $border-width $border-color': '$border',
22
- '$border-style $border-color $border-width': '$border',
23
- '$border-color $border-width $border-style': '$border',
24
- '$border-color $border-style $border-width': '$border',
25
- },
26
- },
27
- 'border color': {
28
- expects: 'a border color variable',
29
- props: 'border{,-top,-right,-bottom,-left}-color',
30
- values: [
31
- '$border-*',
32
- 'transparent',
33
- 'currentColor',
34
- // Match variables in any of the following formats: --color-border-*, --color-*-border-*, --color-*-border, --borderColor-, *borderColor*
35
- /var\(--color-(.+-)*border(-.+)*\)/,
36
- /var\(--color-[^)]+\)/,
37
- /var\(--borderColor-[^)]+\)/,
38
- /var\((.+-)*borderColor(-.+)*\)/,
39
- ],
40
- replacements: {
41
- '$border-gray': '$border-color',
42
- },
43
- },
44
- 'border style': {
45
- expects: 'a border style variable',
46
- props: 'border{,-top,-right,-bottom,-left}-style',
47
- values: ['$border-style', 'none'],
48
- },
49
- 'border width': {
50
- expects: 'a border width variable',
51
- props: 'border{,-top,-right,-bottom,-left}-width',
52
- values: ['$border-width*', '0'],
53
- },
54
- 'border radius': {
55
- expects: 'a border radius variable',
56
- props: 'border{,-{top,bottom}-{left,right}}-radius',
57
- values: ['$border-radius', '0', '50%', 'inherit'],
58
- replacements: {
59
- '100%': '50%',
60
- },
61
- },
1
+ import stylelint from 'stylelint'
2
+ import declarationValueIndex from 'stylelint/lib/utils/declarationValueIndex.cjs'
3
+ import valueParser from 'postcss-value-parser'
4
+ import {walkGroups, primitivesVariables} from './lib/utils.js'
5
+
6
+ const {
7
+ createPlugin,
8
+ utils: {report, ruleMessages, validateOptions},
9
+ } = stylelint
10
+
11
+ export const ruleName = 'primer/borders'
12
+ export const messages = ruleMessages(ruleName, {
13
+ rejected: (value, replacement, propName) => {
14
+ if (propName && propName.includes('radius') && value.includes('borderWidth')) {
15
+ return `Border radius variables can not be used for border widths`
16
+ }
17
+
18
+ if ((propName && propName.includes('width')) || (borderShorthand(propName) && value.includes('borderRadius'))) {
19
+ return `Border width variables can not be used for border radii`
20
+ }
21
+
22
+ if (!replacement) {
23
+ return `Please use a Primer border variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/size#border`
24
+ }
25
+
26
+ return `Please replace '${value}' with a Primer border variable '${replacement['name']}'. https://primer.style/foundations/primitives/size#border`
62
27
  },
63
- 'https://primer.style/css/utilities/borders',
64
- )
28
+ })
29
+
30
+ const variables = primitivesVariables('border')
31
+ const sizes = []
32
+ const radii = []
33
+
34
+ // Props that we want to check
35
+ const propList = ['border', 'border-width', 'border-radius']
36
+ // Values that we want to ignore
37
+ const valueList = ['${']
38
+
39
+ const borderShorthand = prop =>
40
+ /^border(-(top|right|bottom|left|block-start|block-end|inline-start|inline-end))?$/.test(prop)
41
+
42
+ for (const variable of variables) {
43
+ const name = variable['name']
44
+
45
+ if (name.includes('borderWidth')) {
46
+ const value = variable['values']
47
+ .pop()
48
+ .replace(/max|\(|\)/g, '')
49
+ .split(',')[0]
50
+ sizes.push({
51
+ name,
52
+ values: [value],
53
+ })
54
+ }
55
+
56
+ if (name.includes('borderRadius')) {
57
+ radii.push(variable)
58
+ }
59
+ }
60
+
61
+ /** @type {import('stylelint').Rule} */
62
+ const ruleFunction = (primary, secondaryOptions, context) => {
63
+ return (root, result) => {
64
+ const validOptions = validateOptions(result, ruleName, {
65
+ actual: primary,
66
+ possible: [true],
67
+ })
68
+
69
+ if (!validOptions) return
70
+
71
+ root.walkDecls(declNode => {
72
+ const {prop, value} = declNode
73
+
74
+ if (!propList.some(borderProp => prop.startsWith(borderProp))) return
75
+ if (/^border(-(top|right|bottom|left|block-start|block-end|inline-start|inline-end))?-color$/.test(prop)) return
76
+ if (valueList.some(valueToIgnore => value.includes(valueToIgnore))) return
77
+
78
+ const problems = []
79
+
80
+ const parsedValue = walkGroups(valueParser(value), node => {
81
+ const checkForVariable = (vars, nodeValue) =>
82
+ vars.some(variable =>
83
+ new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(nodeValue),
84
+ )
85
+
86
+ // Only check word types. https://github.com/TrySound/postcss-value-parser#word
87
+ if (node.type !== 'word') {
88
+ return
89
+ }
90
+
91
+ // Exact values to ignore.
92
+ if (
93
+ [
94
+ '*',
95
+ '+',
96
+ '-',
97
+ '/',
98
+ '0',
99
+ 'none',
100
+ 'inherit',
101
+ 'initial',
102
+ 'revert',
103
+ 'revert-layer',
104
+ 'unset',
105
+ 'solid',
106
+ 'dashed',
107
+ 'dotted',
108
+ 'transparent',
109
+ ].includes(node.value)
110
+ ) {
111
+ return
112
+ }
113
+
114
+ const valueUnit = valueParser.unit(node.value)
115
+
116
+ if (valueUnit && (valueUnit.unit === '' || !/^-?[0-9.]+$/.test(valueUnit.number))) {
117
+ return
118
+ }
119
+
120
+ // Skip if the value unit isn't a supported unit.
121
+ if (valueUnit && !['px', 'rem', 'em'].includes(valueUnit.unit)) {
122
+ return
123
+ }
124
+
125
+ // if we're looking at the border property that sets color in shorthand, don't bother checking the color
126
+ if (
127
+ // using border shorthand
128
+ borderShorthand(prop) &&
129
+ // includes a color as a third space-separated value
130
+ value.split(' ').length > 2 &&
131
+ // the color in the third space-separated value includes `node.value`
132
+ value
133
+ .split(' ')
134
+ .slice(2)
135
+ .some(color => color.includes(node.value))
136
+ ) {
137
+ return
138
+ }
139
+
140
+ // If the variable is found in the value, skip it.
141
+ if (prop.includes('width') || borderShorthand(prop)) {
142
+ if (checkForVariable(sizes, node.value)) {
143
+ return
144
+ }
145
+ }
146
+
147
+ if (prop.includes('radius')) {
148
+ if (checkForVariable(radii, node.value)) {
149
+ return
150
+ }
151
+ }
152
+
153
+ const replacement = (prop.includes('radius') ? radii : sizes).find(variable =>
154
+ variable.values.includes(node.value.replace('-', '')),
155
+ )
156
+ const fixable = replacement && valueUnit && !valueUnit.number.includes('-')
157
+
158
+ if (fixable && context.fix) {
159
+ node.value = node.value.replace(node.value, `var(${replacement['name']})`)
160
+ } else {
161
+ problems.push({
162
+ index: declarationValueIndex(declNode) + node.sourceIndex,
163
+ endIndex: declarationValueIndex(declNode) + node.sourceIndex + node.value.length,
164
+ message: messages.rejected(node.value, replacement, prop),
165
+ })
166
+ }
167
+
168
+ return
169
+ })
170
+
171
+ if (context.fix) {
172
+ declNode.value = parsedValue.toString()
173
+ }
174
+
175
+ if (problems.length) {
176
+ for (const err of problems) {
177
+ report({
178
+ index: err.index,
179
+ endIndex: err.endIndex,
180
+ message: err.message,
181
+ node: declNode,
182
+ result,
183
+ ruleName,
184
+ })
185
+ }
186
+ }
187
+ })
188
+ }
189
+ }
190
+
191
+ ruleFunction.ruleName = ruleName
192
+ ruleFunction.messages = messages
193
+ ruleFunction.meta = {
194
+ fixable: true,
195
+ }
196
+
197
+ export default createPlugin(ruleName, ruleFunction)
@@ -2,14 +2,17 @@ import {createRequire} from 'node:module'
2
2
 
3
3
  const require = createRequire(import.meta.url)
4
4
 
5
- export async function primitivesVariables(type) {
5
+ export function primitivesVariables(type) {
6
6
  const variables = []
7
7
 
8
8
  const files = []
9
9
  switch (type) {
10
- case 'size':
10
+ case 'spacing':
11
11
  files.push('base/size/size.json')
12
12
  break
13
+ case 'border':
14
+ files.push('functional/size/border.json')
15
+ break
13
16
  }
14
17
 
15
18
  for (const file of files) {
@@ -18,9 +21,7 @@ export async function primitivesVariables(type) {
18
21
 
19
22
  for (const key of Object.keys(data)) {
20
23
  const size = data[key]
21
- const values = size['value']
22
- values.push(`${parseInt(size['original']['value']) + 1}px`)
23
- values.push(`${parseInt(size['original']['value']) - 1}px`)
24
+ const values = typeof size['value'] === 'string' ? [size['value']] : size['value']
24
25
 
25
26
  variables.push({
26
27
  name: `--${size['name']}`,
@@ -31,3 +32,14 @@ export async function primitivesVariables(type) {
31
32
 
32
33
  return variables
33
34
  }
35
+
36
+ export function walkGroups(root, validate) {
37
+ for (const node of root.nodes) {
38
+ if (node.type === 'function') {
39
+ walkGroups(node, validate)
40
+ } else {
41
+ validate(node)
42
+ }
43
+ }
44
+ return root
45
+ }
@@ -1,24 +1,13 @@
1
1
  import stylelint from 'stylelint'
2
2
  import declarationValueIndex from 'stylelint/lib/utils/declarationValueIndex.cjs'
3
3
  import valueParser from 'postcss-value-parser'
4
- import {primitivesVariables} from './lib/primitives.js'
4
+ import {primitivesVariables, walkGroups} from './lib/utils.js'
5
5
 
6
6
  const {
7
7
  createPlugin,
8
8
  utils: {report, ruleMessages, validateOptions},
9
9
  } = stylelint
10
10
 
11
- const walkGroups = (root, validate) => {
12
- for (const node of root.nodes) {
13
- if (node.type === 'function') {
14
- walkGroups(node, validate)
15
- } else {
16
- validate(node)
17
- }
18
- }
19
- return root
20
- }
21
-
22
11
  export const ruleName = 'primer/spacing'
23
12
  export const messages = ruleMessages(ruleName, {
24
13
  rejected: (value, replacement) => {
@@ -30,20 +19,26 @@ export const messages = ruleMessages(ruleName, {
30
19
  },
31
20
  })
32
21
 
33
- const meta = {
34
- fixable: true,
22
+ // Props that we want to check
23
+ const propList = ['padding', 'margin', 'top', 'right', 'bottom', 'left']
24
+ // Values that we want to ignore
25
+ const valueList = ['${']
26
+
27
+ const sizes = primitivesVariables('spacing')
28
+
29
+ // Add +-1px to each value
30
+ for (const size of sizes) {
31
+ const values = size['values']
32
+ const px = parseInt(values.find(value => value.includes('px')))
33
+ if (![2, 6].includes(px)) {
34
+ values.push(`${px + 1}px`)
35
+ values.push(`${px - 1}px`)
36
+ }
35
37
  }
36
38
 
37
39
  /** @type {import('stylelint').Rule} */
38
40
  const ruleFunction = (primary, secondaryOptions, context) => {
39
- return async (root, result) => {
40
- // Props that we want to check
41
- const propList = ['padding', 'margin', 'top', 'right', 'bottom', 'left']
42
- // Values that we want to ignore
43
- const valueList = ['${']
44
-
45
- const sizes = await primitivesVariables('size')
46
-
41
+ return (root, result) => {
47
42
  const validOptions = validateOptions(result, ruleName, {
48
43
  actual: primary,
49
44
  possible: [true],
@@ -128,6 +123,8 @@ const ruleFunction = (primary, secondaryOptions, context) => {
128
123
 
129
124
  ruleFunction.ruleName = ruleName
130
125
  ruleFunction.messages = messages
131
- ruleFunction.meta = meta
126
+ ruleFunction.meta = {
127
+ fixable: true,
128
+ }
132
129
 
133
130
  export default createPlugin(ruleName, ruleFunction)