@primer/stylelint-config 13.0.0-rc.f33e046 → 13.0.0-rc.f7b4bdd

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.f33e046",
3
+ "version": "13.0.0-rc.f7b4bdd",
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.16.0",
48
+ "@primer/primitives": "^8.2.0",
49
49
  "anymatch": "^3.1.1",
50
50
  "postcss-scss": "^4.0.2",
51
51
  "postcss-styled-syntax": "^0.6.4",
@@ -53,30 +53,38 @@
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",
56
57
  "stylelint-no-unsupported-browser-features": "^8.0.0",
57
58
  "stylelint-order": "^6.0.4",
58
59
  "stylelint-scss": "^6.2.0",
59
60
  "stylelint-value-no-unknown-custom-properties": "^6.0.1",
60
61
  "tap-map": "^1.0.0"
61
62
  },
63
+ "overrides": {
64
+ "stylelint-css-modules-no-global-scoped-selector": {
65
+ "stylelint": "$stylelint",
66
+ "postcss-modules-local-by-default": "^4.0.0"
67
+ }
68
+ },
62
69
  "prettier": "@github/prettier-config",
63
70
  "devDependencies": {
64
71
  "@changesets/changelog-github": "^0.5.0",
65
- "@changesets/cli": "2.26.1",
72
+ "@changesets/cli": "2.27.7",
66
73
  "@github/prettier-config": "^0.0.6",
67
- "@rollup/plugin-commonjs": "^25.0.7",
74
+ "@rollup/plugin-commonjs": "^26.0.1",
68
75
  "@rollup/plugin-json": "^6.1.0",
69
76
  "@rollup/plugin-node-resolve": "^15.2.3",
70
- "@typescript-eslint/parser": "^7.7.0",
77
+ "@typescript-eslint/parser": "^8.0.1",
71
78
  "dedent": "^1.5.3",
72
- "eslint": "^8.0.1",
73
- "eslint-plugin-github": "^4.10.2",
79
+ "eslint": "^8.57.0",
80
+ "eslint-plugin-github": "^5.0.1",
81
+ "eslint-plugin-import": "^2.29.1",
74
82
  "eslint-plugin-jest": "^28.2.0",
75
83
  "eslint-plugin-prettier": "^5.1.3",
76
84
  "jest": "^29.7.0",
77
85
  "jest-preset-stylelint": "^7.0.0",
78
86
  "prettier": "^3.2.5",
79
- "rimraf": "^5.0.5",
87
+ "rimraf": "^6.0.1",
80
88
  "rollup": "^4.14.3"
81
89
  },
82
90
  "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)
@@ -0,0 +1,45 @@
1
+ import {createRequire} from 'node:module'
2
+
3
+ const require = createRequire(import.meta.url)
4
+
5
+ export function primitivesVariables(type) {
6
+ const variables = []
7
+
8
+ const files = []
9
+ switch (type) {
10
+ case 'spacing':
11
+ files.push('base/size/size.json')
12
+ break
13
+ case 'border':
14
+ files.push('functional/size/border.json')
15
+ break
16
+ }
17
+
18
+ for (const file of files) {
19
+ // eslint-disable-next-line import/no-dynamic-require
20
+ const data = require(`@primer/primitives/dist/styleLint/${file}`)
21
+
22
+ for (const key of Object.keys(data)) {
23
+ const size = data[key]
24
+ const values = typeof size['value'] === 'string' ? [size['value']] : size['value']
25
+
26
+ variables.push({
27
+ name: `--${size['name']}`,
28
+ values,
29
+ })
30
+ }
31
+ }
32
+
33
+ return variables
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,69 +1,60 @@
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, walkGroups} from './lib/utils.js'
4
5
 
5
- // TODO: Pull this in from primer/primitives
6
- const spacerValues = {
7
- '$spacer-1': '4px',
8
- '$spacer-2': '8px',
9
- '$spacer-3': '16px',
10
- '$spacer-4': '24px',
11
- '$spacer-5': '32px',
12
- '$spacer-6': '40px',
13
- '$spacer-7': '48px',
14
- '$spacer-8': '64px',
15
- '$spacer-9': '80px',
16
- '$spacer-10': '96px',
17
- '$spacer-11': '112px',
18
- '$spacer-12': '128px',
19
- '$em-spacer-1': '0.0625em',
20
- '$em-spacer-2': '0.125em',
21
- '$em-spacer-3': '0.25em',
22
- '$em-spacer-4': '0.375em',
23
- '$em-spacer-5': '0.5em',
24
- '$em-spacer-6': '0.75em',
25
- }
6
+ const {
7
+ createPlugin,
8
+ utils: {report, ruleMessages, validateOptions},
9
+ } = stylelint
26
10
 
27
11
  export const ruleName = 'primer/spacing'
28
- export const messages = stylelint.utils.ruleMessages(ruleName, {
12
+ export const messages = ruleMessages(ruleName, {
29
13
  rejected: (value, replacement) => {
30
- if (replacement === null) {
31
- return `Please use a primer spacer variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/css/storybook/?path=/docs/support-spacing--docs`
14
+ if (!replacement) {
15
+ return `Please use a primer size variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/size`
32
16
  }
33
17
 
34
- return `Please replace ${value} with spacing variable '${replacement}'.`
18
+ return `Please replace '${value}' with size variable '${replacement['name']}'. https://primer.style/foundations/primitives/size`
35
19
  },
36
20
  })
37
21
 
38
- const walkGroups = (root, validate) => {
39
- for (const node of root.nodes) {
40
- if (node.type === 'function') {
41
- walkGroups(node, validate)
42
- } else {
43
- validate(node)
44
- }
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`)
45
36
  }
46
- return root
47
37
  }
48
38
 
49
- // eslint-disable-next-line no-unused-vars
50
- export default stylelint.createPlugin(ruleName, (enabled, options = {}, context) => {
51
- if (!enabled) {
52
- return noop
53
- }
39
+ /** @type {import('stylelint').Rule} */
40
+ const ruleFunction = (primary, secondaryOptions, context) => {
41
+ return (root, result) => {
42
+ const validOptions = validateOptions(result, ruleName, {
43
+ actual: primary,
44
+ possible: [true],
45
+ })
54
46
 
55
- const lintResult = (root, result) => {
56
- root.walk(decl => {
57
- if (decl.type !== 'decl' || !decl.prop.match(/^(padding|margin)/)) {
58
- return noop
59
- }
47
+ if (!validOptions) return
60
48
 
61
- const problems = []
49
+ root.walkDecls(declNode => {
50
+ const {prop, value} = declNode
62
51
 
63
- const parsedValue = walkGroups(valueParser(decl.value), node => {
64
- // Remove leading negative sign, if any.
65
- const cleanValue = node.value.replace(/^-/g, '')
52
+ if (!propList.some(spacingProp => prop.startsWith(spacingProp))) return
53
+ if (valueList.some(valueToIgnore => value.includes(valueToIgnore))) return
66
54
 
55
+ const problems = []
56
+
57
+ const parsedValue = walkGroups(valueParser(value), node => {
67
58
  // Only check word types. https://github.com/TrySound/postcss-value-parser#word
68
59
  if (node.type !== 'word') {
69
60
  return
@@ -74,30 +65,36 @@ export default stylelint.createPlugin(ruleName, (enabled, options = {}, context)
74
65
  return
75
66
  }
76
67
 
77
- const valueUnit = valueParser.unit(cleanValue)
68
+ const valueUnit = valueParser.unit(node.value)
69
+
70
+ if (valueUnit && (valueUnit.unit === '' || !/^-?[0-9.]+$/.test(valueUnit.number))) {
71
+ return
72
+ }
78
73
 
79
- if (valueUnit && (valueUnit.unit === '' || !/^[0-9.]+$/.test(valueUnit.number))) {
74
+ // Skip if the value unit isn't a supported unit.
75
+ if (valueUnit && !['px', 'rem', 'em'].includes(valueUnit.unit)) {
80
76
  return
81
77
  }
82
78
 
83
- // If the a variable is found in the value, skip it.
79
+ // If the variable is found in the value, skip it.
84
80
  if (
85
- Object.keys(spacerValues).some(variable =>
86
- new RegExp(`${variable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(cleanValue),
81
+ sizes.some(variable =>
82
+ new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(node.value),
87
83
  )
88
84
  ) {
89
85
  return
90
86
  }
91
87
 
92
- const replacement = Object.keys(spacerValues).find(spacer => spacerValues[spacer] === cleanValue) || null
93
- const valueMatch = replacement ? spacerValues[replacement] : node.value
88
+ const replacement = sizes.find(variable => variable.values.includes(node.value.replace('-', '')))
89
+ const fixable = replacement && valueUnit && !valueUnit.number.includes('-')
94
90
 
95
- if (replacement && context.fix) {
96
- node.value = node.value.replace(valueMatch, replacement)
91
+ if (fixable && context.fix) {
92
+ node.value = node.value.replace(node.value, `var(${replacement['name']})`)
97
93
  } else {
98
94
  problems.push({
99
- index: declarationValueIndex(decl) + node.sourceIndex,
100
- message: messages.rejected(valueMatch, replacement),
95
+ index: declarationValueIndex(declNode) + node.sourceIndex,
96
+ endIndex: declarationValueIndex(declNode) + node.sourceIndex + node.value.length,
97
+ message: messages.rejected(node.value, replacement),
101
98
  })
102
99
  }
103
100
 
@@ -105,15 +102,16 @@ export default stylelint.createPlugin(ruleName, (enabled, options = {}, context)
105
102
  })
106
103
 
107
104
  if (context.fix) {
108
- decl.value = parsedValue.toString()
105
+ declNode.value = parsedValue.toString()
109
106
  }
110
107
 
111
108
  if (problems.length) {
112
109
  for (const err of problems) {
113
- stylelint.utils.report({
110
+ report({
114
111
  index: err.index,
112
+ endIndex: err.endIndex,
115
113
  message: err.message,
116
- node: decl,
114
+ node: declNode,
117
115
  result,
118
116
  ruleName,
119
117
  })
@@ -121,8 +119,12 @@ export default stylelint.createPlugin(ruleName, (enabled, options = {}, context)
121
119
  }
122
120
  })
123
121
  }
122
+ }
124
123
 
125
- return lintResult
126
- })
124
+ ruleFunction.ruleName = ruleName
125
+ ruleFunction.messages = messages
126
+ ruleFunction.meta = {
127
+ fixable: true,
128
+ }
127
129
 
128
- function noop() {}
130
+ export default createPlugin(ruleName, ruleFunction)