Skip to content

Commit 229b3a3

Browse files
committed
Add stylelint plugin to ensure theme consistency
1 parent 4127bb4 commit 229b3a3

File tree

2 files changed

+97
-1
lines changed

2 files changed

+97
-1
lines changed

.stylelintrc.json

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
{
22
"extends": "stylelint-config-standard",
3-
"plugins": ["stylelint-declaration-strict-value", "stylelint-use-logical"],
3+
"plugins": [
4+
"stylelint-declaration-strict-value",
5+
"stylelint-use-logical",
6+
"./postcss/validateThemeVariables.js"
7+
],
48
"ignoreFiles": ["**/*.tsx"],
59
"rules": {
610
"function-no-unknown": [
@@ -33,6 +37,16 @@
3337
{
3438
"ignoreShorthands": ["/grid/"]
3539
}
40+
],
41+
"kitsu-io/validate-theme-variables": [
42+
"warning",
43+
{
44+
"files": [
45+
"./src/styles/themes/light.css",
46+
"./src/styles/themes/dark.css",
47+
"./src/styles/themes/oled.css"
48+
]
49+
}
3650
]
3751
},
3852
"overrides": [

postcss/validateThemeVariables.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// @ts-check
2+
const stylelint = require('stylelint');
3+
const fs = require('fs');
4+
const postcss = require('postcss');
5+
6+
const andify = (arr) => {
7+
if (arr.length === 0) {
8+
return '';
9+
} else if (arr.length === 1) {
10+
return arr[0];
11+
} else if (arr.length === 2) {
12+
return arr.join(' and ');
13+
} else {
14+
return `${arr.slice(0, -1).join(', ')}, and ${arr.slice(-1)}`;
15+
}
16+
};
17+
18+
const ruleName = 'kitsu-io/validate-theme-variables';
19+
const messages = stylelint.utils.ruleMessages(ruleName, {
20+
missing: (property, missing) =>
21+
`Variable ${property} is missing from ${andify(missing)}.`,
22+
});
23+
const meta = {
24+
url: 'https://github.com/hummingbird-me/kitsu-web/blob/the-future/postcss/README.md',
25+
};
26+
27+
/** @type {import('stylelint').Rule} */
28+
const ruleFunction = (primaryOption, secondaryOptionObject) => {
29+
/** @type {string[]} */
30+
const files = secondaryOptionObject?.files ?? [];
31+
32+
const fileVariables = {};
33+
for (const file of files) {
34+
const set = (fileVariables[file] = new Set());
35+
postcss()
36+
.process(fs.readFileSync(file, 'utf8'))
37+
.root.walkDecls(/^--/, (decl) => set.add(decl.prop), {});
38+
}
39+
40+
return (postcssRoot, postcssResult) => {
41+
const validOptions = stylelint.utils.validateOptions(
42+
postcssResult,
43+
ruleName,
44+
{
45+
actual: primaryOption,
46+
possible: [true, false, 'warning', 'error'],
47+
},
48+
{
49+
actual: secondaryOptionObject,
50+
possible: {
51+
files: (value) => typeof value === 'string',
52+
},
53+
},
54+
);
55+
56+
if (!validOptions) return;
57+
if (!primaryOption) return;
58+
59+
postcssRoot.walkDecls(/^--/, (decl) => {
60+
const missing = Object.keys(fileVariables).filter(
61+
(file) => !fileVariables[file].has(decl.prop),
62+
);
63+
64+
if (missing.length > 0) {
65+
stylelint.utils.report({
66+
ruleName,
67+
node: decl,
68+
endIndex: decl.prop.length,
69+
result: postcssResult,
70+
message: messages.missing(decl.prop, missing),
71+
severity: primaryOption === 'warning' ? 'warning' : 'error',
72+
});
73+
}
74+
});
75+
};
76+
};
77+
78+
ruleFunction.ruleName = ruleName;
79+
ruleFunction.messages = messages;
80+
ruleFunction.meta = meta;
81+
82+
module.exports = stylelint.createPlugin(ruleName, ruleFunction);

0 commit comments

Comments
 (0)