@primer/stylelint-config 13.0.0-rc.77d8c5f → 13.0.0-rc.7a254e8

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.77d8c5f",
3
+ "version": "13.0.0-rc.7a254e8",
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)
@@ -0,0 +1,49 @@
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
+ }
21
+
22
+ for (const file of files) {
23
+ // eslint-disable-next-line import/no-dynamic-require
24
+ const data = require(`@primer/primitives/dist/styleLint/${file}`)
25
+
26
+ for (const key of Object.keys(data)) {
27
+ const size = data[key]
28
+ const values = typeof size['value'] === 'string' ? [size['value']] : size['value']
29
+
30
+ variables.push({
31
+ name: `--${size['name']}`,
32
+ values,
33
+ })
34
+ }
35
+ }
36
+
37
+ return variables
38
+ }
39
+
40
+ export function walkGroups(root, validate) {
41
+ for (const node of root.nodes) {
42
+ if (node.type === 'function') {
43
+ walkGroups(node, validate)
44
+ } else {
45
+ validate(node)
46
+ }
47
+ }
48
+ return root
49
+ }
@@ -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)
@@ -1,24 +1,186 @@
1
- import {createVariableRule} from './lib/variable-rules.js'
2
-
3
- export default createVariableRule(
4
- 'primer/typography',
5
- {
6
- 'font-size': {
7
- expects: 'a font-size variable',
8
- values: ['$body-font-size', '$h{000,00,0,1,2,3,4,5,6}-size', '$font-size-*', '1', '1em', 'inherit'],
9
- },
10
- 'font-weight': {
11
- props: 'font-weight',
12
- values: ['$font-weight-*', 'inherit'],
13
- replacements: {
14
- bold: '$font-weight-bold',
15
- normal: '$font-weight-normal',
16
- },
17
- },
18
- 'line-height': {
19
- props: 'line-height',
20
- values: ['$body-line-height', '$lh-*', '0', '1', '1em', 'inherit'],
21
- },
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/typography'
11
+ export const messages = ruleMessages(ruleName, {
12
+ rejected: (value, replacement) => {
13
+ // no possible replacement
14
+ if (!replacement) {
15
+ return `Please use a Primer typography variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/typography`
16
+ }
17
+
18
+ // multiple possible replacements
19
+ if (replacement.length) {
20
+ return `Please use one of the following Primer typography variables instead of '${value}': ${replacement.map(replacementObj => `'${replacementObj.name}'`).join(', ')}. https://primer.style/foundations/primitives/typography`
21
+ }
22
+
23
+ // one possible replacement
24
+ return `Please replace '${value}' with Primer typography variable '${replacement['name']}'. https://primer.style/foundations/primitives/typography`
22
25
  },
23
- 'https://primer.style/css/utilities/typography',
24
- )
26
+ })
27
+
28
+ const fontWeightKeywordMap = {
29
+ normal: 400,
30
+ bold: 600,
31
+ bolder: 600,
32
+ lighter: 300,
33
+ }
34
+ const getClosestFontWeight = (goalWeightNumber, fontWeightsTokens) => {
35
+ return fontWeightsTokens.reduce((prev, curr) =>
36
+ Math.abs(curr.values - goalWeightNumber) < Math.abs(prev.values - goalWeightNumber) ? curr : prev,
37
+ ).values
38
+ }
39
+
40
+ const variables = primitivesVariables('typography')
41
+ const fontSizes = []
42
+ const fontWeights = []
43
+ const lineHeights = []
44
+ const fontStacks = []
45
+ const fontShorthands = []
46
+
47
+ // Props that we want to check for typography variables
48
+ const propList = ['font-size', 'font-weight', 'line-height', 'font-family', 'font']
49
+
50
+ for (const variable of variables) {
51
+ const name = variable['name']
52
+
53
+ if (name.includes('size')) {
54
+ fontSizes.push(variable)
55
+ }
56
+
57
+ if (name.includes('weight')) {
58
+ fontWeights.push(variable)
59
+ }
60
+
61
+ if (name.includes('lineHeight')) {
62
+ lineHeights.push(variable)
63
+ }
64
+
65
+ if (name.includes('fontStack')) {
66
+ fontStacks.push(variable)
67
+ }
68
+
69
+ if (name.includes('shorthand')) {
70
+ fontShorthands.push(variable)
71
+ }
72
+ }
73
+
74
+ /** @type {import('stylelint').Rule} */
75
+ const ruleFunction = (primary, secondaryOptions, context) => {
76
+ return (root, result) => {
77
+ const validOptions = validateOptions(result, ruleName, {
78
+ actual: primary,
79
+ possible: [true],
80
+ })
81
+ let validValues = []
82
+
83
+ if (!validOptions) return
84
+
85
+ root.walkDecls(declNode => {
86
+ const {prop, value} = declNode
87
+
88
+ if (!propList.some(typographyProp => prop.startsWith(typographyProp))) return
89
+
90
+ const problems = []
91
+
92
+ const checkForVariable = (vars, nodeValue) =>
93
+ vars.some(variable =>
94
+ new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(nodeValue),
95
+ )
96
+
97
+ // Exact values to ignore.
98
+ if (value === 'inherit') {
99
+ return
100
+ }
101
+
102
+ switch (prop) {
103
+ case 'font-size':
104
+ validValues = fontSizes
105
+ break
106
+ case 'font-weight':
107
+ validValues = fontWeights
108
+ break
109
+ case 'line-height':
110
+ validValues = lineHeights
111
+ break
112
+ case 'font-family':
113
+ validValues = fontStacks
114
+ break
115
+ case 'font':
116
+ validValues = fontShorthands
117
+ break
118
+ default:
119
+ validValues = []
120
+ }
121
+
122
+ if (checkForVariable(validValues, value)) {
123
+ return
124
+ }
125
+
126
+ const getReplacements = () => {
127
+ const replacementTokens = validValues.filter(variable => {
128
+ if (!(variable.values instanceof Array)) {
129
+ let nodeValue = value
130
+
131
+ if (prop === 'font-weight') {
132
+ nodeValue = getClosestFontWeight(fontWeightKeywordMap[value] || value, fontWeights)
133
+ }
134
+
135
+ return variable.values.toString() === nodeValue.toString()
136
+ }
137
+
138
+ return variable.values.includes(value.replace('-', ''))
139
+ })
140
+
141
+ if (!replacementTokens.length) {
142
+ return
143
+ }
144
+
145
+ if (replacementTokens.length > 1) {
146
+ return replacementTokens
147
+ }
148
+
149
+ return replacementTokens[0]
150
+ }
151
+ const replacement = getReplacements()
152
+ const fixable = replacement && !replacement.length
153
+
154
+ if (fixable && context.fix) {
155
+ declNode.value = value.replace(value, `var(${replacement['name']})`)
156
+ } else {
157
+ problems.push({
158
+ index: declarationValueIndex(declNode),
159
+ endIndex: declarationValueIndex(declNode) + value.length,
160
+ message: messages.rejected(value, replacement, prop),
161
+ })
162
+ }
163
+
164
+ if (problems.length) {
165
+ for (const err of problems) {
166
+ report({
167
+ index: err.index,
168
+ endIndex: err.endIndex,
169
+ message: err.message,
170
+ node: declNode,
171
+ result,
172
+ ruleName,
173
+ })
174
+ }
175
+ }
176
+ })
177
+ }
178
+ }
179
+
180
+ ruleFunction.ruleName = ruleName
181
+ ruleFunction.messages = messages
182
+ ruleFunction.meta = {
183
+ fixable: true,
184
+ }
185
+
186
+ export default createPlugin(ruleName, ruleFunction)