Skip to content

Commit e0210d3

Browse files
committed
adds first draft of primer/box-shadow plugin
1 parent 22e67a6 commit e0210d3

File tree

3 files changed

+204
-48
lines changed

3 files changed

+204
-48
lines changed

__tests__/box-shadow.js

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,53 @@
1-
import dedent from 'dedent'
2-
import stylelint from 'stylelint'
3-
import pluginPath from '../plugins/box-shadow.js'
1+
import plugin from '../plugins/box-shadow.js'
42

5-
const ruleName = 'primer/box-shadow'
6-
const configWithOptions = args => ({
7-
plugins: [pluginPath],
8-
rules: {
9-
[ruleName]: args,
10-
},
11-
})
3+
const plugins = [plugin]
4+
const {
5+
ruleName,
6+
rule: {messages},
7+
} = plugin
128

13-
describe(ruleName, () => {
14-
it('does not report properties with valid shadow', () => {
15-
return stylelint
16-
.lint({
17-
code: dedent`
18-
.x { box-shadow: var(--color-shadow-primary); }
19-
.y { box-shadow: var(--color-btn-shadow-hover); }
20-
.z { box-shadow: var(--color-diff-deletion-shadow); }
21-
.a { box-shadow: var(--color-shadow); }
22-
`,
23-
config: configWithOptions(true),
24-
})
25-
.then(data => {
26-
expect(data).not.toHaveErrored()
27-
expect(data).toHaveWarningsLength(0)
28-
})
29-
})
9+
// General Tests
10+
testRule({
11+
plugins,
12+
ruleName,
13+
config: [true, {}],
14+
fix: true,
15+
cache: false,
16+
accept: [
17+
{
18+
code: '.x { box-shadow: var(--shadow-resting-medium); }',
19+
description: 'CSS > Accepts box shadow variables',
20+
},
21+
],
22+
reject: [
23+
{
24+
code: '.x { box-shadow: 1px 2px 3px 4px #000000; }',
25+
unfixable: true,
26+
message: messages.rejected('1px 2px 3px 4px #000000'),
27+
line: 1,
28+
column: 18,
29+
endColumn: 41,
30+
description: 'CSS > Errors on value not in box-shadow list',
31+
},
32+
// Light mode shadow replacement
33+
{
34+
code: '.x { box-shadow: 0px 3px 6px 0px #424a531f; }',
35+
fixed: '.x { box-shadow: var(--shadow-resting-medium); }',
36+
message: messages.rejected('0px 3px 6px 0px #424a531f', {name: '--shadow-resting-medium'}),
37+
line: 1,
38+
column: 18,
39+
endColumn: 43,
40+
description: "CSS > Replaces '0px 3px 6px 0px #424a531f' with 'var(--shadow-resting-medium)'.",
41+
},
42+
// Dark mode shadow replacement
43+
{
44+
code: '.x { box-shadow: 0px 3px 6px 0px #010409cc; }',
45+
fixed: '.x { box-shadow: var(--shadow-resting-medium); }',
46+
message: messages.rejected('0px 3px 6px 0px #010409cc', {name: '--shadow-resting-medium'}),
47+
line: 1,
48+
column: 18,
49+
endColumn: 43,
50+
description: "CSS > Replaces '0px 3px 6px 0px #010409cc' with 'var(--shadow-resting-medium)'.",
51+
},
52+
],
3053
})

plugins/box-shadow.js

Lines changed: 101 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,102 @@
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+
// TODO: Decide if we just want to link to Storybook, or if we want to add new pages to https://primer.style/foundations/primitives
14+
if (!replacement) {
15+
return `Please use a Primer box-shadow variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/primitives/storybook/?path=/story/color-functional-shadows--shadows`
16+
}
17+
18+
return `Please replace '${value}' with a Primer box-shadow variable '${replacement['name']}'. https://primer.style/primitives/storybook/?path=/story/color-functional-shadows--shadows`
2019
},
21-
'https://primer.style/css/utilities/box-shadow',
22-
)
20+
})
21+
22+
const variables = primitivesVariables('box-shadow')
23+
const shadows = []
24+
25+
for (const variable of variables) {
26+
const name = variable['name']
27+
28+
// TODO: Decide if this is safe. Someday we might have variables that
29+
// have 'shadow' in the name but aren't full box-shadows.
30+
if (name.includes('shadow')) {
31+
shadows.push(variable)
32+
}
33+
}
34+
35+
console.log(shadows)
36+
37+
/** @type {import('stylelint').Rule} */
38+
const ruleFunction = (primary, secondaryOptions, context) => {
39+
return (root, result) => {
40+
const validOptions = validateOptions(result, ruleName, {
41+
actual: primary,
42+
possible: [true],
43+
})
44+
const validValues = shadows
45+
46+
if (!validOptions) return
47+
48+
// TODO: determine if it's safe to look at the whole declaration value
49+
// instead of each node in the value
50+
root.walkDecls(declNode => {
51+
const {prop, value} = declNode
52+
53+
if (prop !== 'box-shadow') return
54+
55+
const problems = []
56+
57+
const checkForVariable = (vars, nodeValue) => {
58+
59+
return vars.some(variable =>
60+
new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(nodeValue),
61+
)
62+
}
63+
64+
if (checkForVariable(validValues, value)) {
65+
return
66+
}
67+
68+
const replacement = validValues.find(variable => variable.values.includes(value))
69+
70+
if (replacement && context.fix) {
71+
declNode.value = value.replace(value, `var(${replacement['name']})`)
72+
} else {
73+
problems.push({
74+
index: declarationValueIndex(declNode),
75+
endIndex: declarationValueIndex(declNode) + value.length,
76+
message: messages.rejected(value, replacement),
77+
})
78+
}
79+
80+
if (problems.length) {
81+
for (const err of problems) {
82+
report({
83+
index: err.index,
84+
endIndex: err.endIndex,
85+
message: err.message,
86+
node: declNode,
87+
result,
88+
ruleName,
89+
})
90+
}
91+
}
92+
})
93+
}
94+
}
95+
96+
ruleFunction.ruleName = ruleName
97+
ruleFunction.messages = messages
98+
ruleFunction.meta = {
99+
fixable: true,
100+
}
101+
102+
export default createPlugin(ruleName, ruleFunction)

plugins/lib/utils.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/dark.json')
22+
files.push('functional/themes/light.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+
}

0 commit comments

Comments
 (0)