@primer/stylelint-config 13.0.0-rc.fd47ce2 → 13.0.0

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.fd47ce2",
3
+ "version": "13.0.0",
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": "^8.2.0",
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,37 +53,32 @@
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",
86
- "rollup": "^4.14.3"
80
+ "rimraf": "^6.0.1",
81
+ "rollup": "^4.21.1"
87
82
  },
88
83
  "jest": {
89
84
  "transform": {},
@@ -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)
@@ -1,22 +1,98 @@
1
- import {createVariableRule} from './lib/variable-rules.js'
2
-
3
- export default createVariableRule(
4
- 'primer/box-shadow',
5
- {
6
- 'box shadow': {
7
- expects: 'a box-shadow variable',
8
- props: 'box-shadow',
9
- values: [
10
- '$box-shadow*',
11
- '$*-shadow',
12
- 'none',
13
- // Match variables in any of the following formats: --color-shadow-*, --color-*-shadow-*, --color-*-shadow, --shadow-*, *shadow*
14
- /var\(--color-(.+-)*shadow(-.+)*\)/,
15
- /var\(--shadow(-.+)*\)/,
16
- /var\((.+-)*shadow(-.+)*\)/,
17
- ],
18
- singular: true,
19
- },
1
+ import stylelint from 'stylelint'
2
+ import declarationValueIndex from 'stylelint/lib/utils/declarationValueIndex.cjs'
3
+ import {primitivesVariables} from './lib/utils.js'
4
+
5
+ const {
6
+ createPlugin,
7
+ utils: {report, ruleMessages, validateOptions},
8
+ } = stylelint
9
+
10
+ export const ruleName = 'primer/box-shadow'
11
+ export const messages = ruleMessages(ruleName, {
12
+ rejected: (value, replacement) => {
13
+ if (!replacement) {
14
+ return `Please use a Primer box-shadow variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/color#shadow or https://primer.style/foundations/primitives/size#border-size`
15
+ }
16
+
17
+ return `Please replace '${value}' with a Primer box-shadow variable '${replacement['name']}'. https://primer.style/foundations/primitives/color#shadow or https://primer.style/foundations/primitives/size#border-size`
20
18
  },
21
- 'https://primer.style/css/utilities/box-shadow',
22
- )
19
+ })
20
+
21
+ const variables = primitivesVariables('box-shadow')
22
+ const shadows = []
23
+
24
+ for (const variable of variables) {
25
+ const name = variable['name']
26
+
27
+ // TODO: Decide if this is safe. Someday we might have variables that
28
+ // have 'shadow' in the name but aren't full box-shadows.
29
+ if (name.includes('shadow') || name.includes('boxShadow')) {
30
+ shadows.push(variable)
31
+ }
32
+ }
33
+
34
+ /** @type {import('stylelint').Rule} */
35
+ const ruleFunction = (primary, secondaryOptions, context) => {
36
+ return (root, result) => {
37
+ const validOptions = validateOptions(result, ruleName, {
38
+ actual: primary,
39
+ possible: [true],
40
+ })
41
+ const validValues = shadows
42
+
43
+ if (!validOptions) return
44
+
45
+ root.walkDecls(declNode => {
46
+ const {prop, value} = declNode
47
+
48
+ if (prop !== 'box-shadow') return
49
+
50
+ if (value === 'none') return
51
+
52
+ const problems = []
53
+
54
+ const checkForVariable = (vars, nodeValue) => {
55
+ return vars.some(variable =>
56
+ new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(nodeValue),
57
+ )
58
+ }
59
+
60
+ if (checkForVariable(validValues, value)) {
61
+ return
62
+ }
63
+
64
+ const replacement = validValues.find(variable => variable.values.includes(value))
65
+
66
+ if (replacement && context.fix) {
67
+ declNode.value = value.replace(value, `var(${replacement['name']})`)
68
+ } else {
69
+ problems.push({
70
+ index: declarationValueIndex(declNode),
71
+ endIndex: declarationValueIndex(declNode) + value.length,
72
+ message: messages.rejected(value, replacement),
73
+ })
74
+ }
75
+
76
+ if (problems.length) {
77
+ for (const err of problems) {
78
+ report({
79
+ index: err.index,
80
+ endIndex: err.endIndex,
81
+ message: err.message,
82
+ node: declNode,
83
+ result,
84
+ ruleName,
85
+ })
86
+ }
87
+ }
88
+ })
89
+ }
90
+ }
91
+
92
+ ruleFunction.ruleName = ruleName
93
+ ruleFunction.messages = messages
94
+ ruleFunction.meta = {
95
+ fixable: true,
96
+ }
97
+
98
+ export default createPlugin(ruleName, ruleFunction)
@@ -0,0 +1,53 @@
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
+ case 'typography':
17
+ files.push('base/typography/typography.json')
18
+ files.push('functional/typography/typography.json')
19
+ break
20
+ case 'box-shadow':
21
+ files.push('functional/themes/light.json')
22
+ files.push('functional/size/border.json')
23
+ break
24
+ }
25
+
26
+ for (const file of files) {
27
+ // eslint-disable-next-line import/no-dynamic-require
28
+ const data = require(`@primer/primitives/dist/styleLint/${file}`)
29
+
30
+ for (const key of Object.keys(data)) {
31
+ const size = data[key]
32
+ const values = typeof size['value'] === 'string' ? [size['value']] : size['value']
33
+
34
+ variables.push({
35
+ name: `--${size['name']}`,
36
+ values,
37
+ })
38
+ }
39
+ }
40
+
41
+ return variables
42
+ }
43
+
44
+ export function walkGroups(root, validate) {
45
+ for (const node of root.nodes) {
46
+ if (node.type === 'function') {
47
+ walkGroups(node, validate)
48
+ } else {
49
+ validate(node)
50
+ }
51
+ }
52
+ return root
53
+ }
@@ -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)