Skip to content

Commit 1a75a0e

Browse files
Parse theme.json to recognize WordPress custom properties in stylelint, Ref: DEV-758
1 parent 7510efd commit 1a75a0e

File tree

3 files changed

+126
-3
lines changed

3 files changed

+126
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### 1.0.8: 2026-02-11
2+
3+
* Parse theme.json to recognize WordPress custom properties in stylelint, Ref: DEV-758
4+
15
### 1.0.7: 2026-02-06
26

37
* Add exclude patterns for wp-scripts generated blocks files, Ref: DEV-748

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@digitoimistodude/stylelint-config",
3-
"version": "1.0.7",
3+
"version": "1.0.8",
44
"description": "Dude's shareable stylelint config for SCSS",
55
"main": "stylelint.config.js",
66
"repository": {

stylelint.config.js

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,125 @@ function findDudestackThemeCss() {
4949

5050
const globalCssPath = findGlobalCss();
5151

52+
// Find theme.json and extract WordPress custom properties
53+
// WordPress generates --wp--preset--*--{slug} and --wp--custom--* at runtime
54+
// from theme.json, so they never appear in compiled CSS files
55+
// Reference: https://linear.app/dude/issue/DEV-758
56+
function findThemeJson() {
57+
const possiblePaths = [
58+
'theme.json',
59+
...findDudestackThemeJson()
60+
];
61+
62+
for (const jsonPath of possiblePaths) {
63+
if (fs.existsSync(jsonPath)) {
64+
return jsonPath;
65+
}
66+
}
67+
68+
return null;
69+
}
70+
71+
function findDudestackThemeJson() {
72+
const themesDir = 'content/themes';
73+
if (!fs.existsSync(themesDir)) {
74+
return [];
75+
}
76+
77+
try {
78+
return fs.readdirSync(themesDir, { withFileTypes: true })
79+
.filter(dirent => dirent.isDirectory())
80+
.map(dirent => path.join(themesDir, dirent.name, 'theme.json'));
81+
} catch {
82+
return [];
83+
}
84+
}
85+
86+
function flattenCustomSettings(obj, prefix, result) {
87+
for (const [key, value] of Object.entries(obj)) {
88+
const kebabKey = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
89+
const varName = `${prefix}--${kebabKey}`;
90+
91+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
92+
flattenCustomSettings(value, varName, result);
93+
} else {
94+
result[varName] = String(value);
95+
}
96+
}
97+
}
98+
99+
function getThemeJsonCustomProperties() {
100+
const themeJsonPath = findThemeJson();
101+
if (!themeJsonPath) {
102+
return null;
103+
}
104+
105+
try {
106+
const themeJson = JSON.parse(fs.readFileSync(themeJsonPath, 'utf8'));
107+
const settings = themeJson.settings || {};
108+
const customProperties = {};
109+
110+
// --wp--preset--color--{slug}
111+
const palette = (settings.color && settings.color.palette) || [];
112+
for (const item of palette) {
113+
customProperties[`--wp--preset--color--${item.slug}`] = item.color || '';
114+
}
115+
116+
// --wp--preset--gradient--{slug}
117+
const gradients = (settings.color && settings.color.gradients) || [];
118+
for (const item of gradients) {
119+
customProperties[`--wp--preset--gradient--${item.slug}`] = item.gradient || '';
120+
}
121+
122+
// --wp--preset--font-family--{slug}
123+
const fontFamilies = (settings.typography && settings.typography.fontFamilies) || [];
124+
for (const item of fontFamilies) {
125+
customProperties[`--wp--preset--font-family--${item.slug}`] = item.fontFamily || '';
126+
}
127+
128+
// --wp--preset--font-size--{slug}
129+
const fontSizes = (settings.typography && settings.typography.fontSizes) || [];
130+
for (const item of fontSizes) {
131+
customProperties[`--wp--preset--font-size--${item.slug}`] = item.size || '';
132+
}
133+
134+
// --wp--preset--spacing--{slug}
135+
const spacingSizes = (settings.spacing && settings.spacing.spacingSizes) || [];
136+
for (const item of spacingSizes) {
137+
customProperties[`--wp--preset--spacing--${item.slug}`] = item.size || '';
138+
}
139+
140+
// --wp--preset--shadow--{slug}
141+
const shadows = (settings.shadow && settings.shadow.presets) || [];
142+
for (const item of shadows) {
143+
customProperties[`--wp--preset--shadow--${item.slug}`] = item.shadow || '';
144+
}
145+
146+
// --wp--custom--{key} (nested objects with -- separator)
147+
if (settings.custom) {
148+
flattenCustomSettings(settings.custom, '--wp--custom', customProperties);
149+
}
150+
151+
// --wp--style--global--content-size and --wp--style--global--wide-size
152+
if (settings.layout) {
153+
if (settings.layout.contentSize) {
154+
customProperties['--wp--style--global--content-size'] = settings.layout.contentSize;
155+
}
156+
if (settings.layout.wideSize) {
157+
customProperties['--wp--style--global--wide-size'] = settings.layout.wideSize;
158+
}
159+
}
160+
161+
return Object.keys(customProperties).length > 0
162+
? { customProperties }
163+
: null;
164+
} catch {
165+
return null;
166+
}
167+
}
168+
169+
const themeJsonProperties = getThemeJsonCustomProperties();
170+
52171
module.exports = {
53172
defaultSeverity: 'warning',
54173
plugins: [
@@ -166,11 +285,11 @@ module.exports = {
166285
]
167286
}
168287
],
169-
'csstools/value-no-unknown-custom-properties': globalCssPath
288+
'csstools/value-no-unknown-custom-properties': globalCssPath || themeJsonProperties
170289
? [
171290
true,
172291
{
173-
importFrom: [globalCssPath]
292+
importFrom: [globalCssPath, themeJsonProperties].filter(Boolean)
174293
}
175294
]
176295
: null,

0 commit comments

Comments
 (0)