Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .stylelintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ build
build-style
node_modules
packages/stylelint-config/test
packages/theme/src/stylelint-plugins/test/fixtures
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/stylelint-config/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New Features

- Added `plugin-wpds/no-unknown-ds-tokens` and `plugin-wpds/no-setting-wpds-custom-properties` rules from `@wordpress/theme` to catch design token misuse ([#74226](https://github.com/WordPress/gutenberg/pull/74226)).

## 23.28.0 (2025-11-26)

## 23.27.0 (2025-11-12)
Expand Down
6 changes: 6 additions & 0 deletions packages/stylelint-config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
/** @type {import('stylelint').Config} */
module.exports = {
extends: [ 'stylelint-config-recommended' ].map( require.resolve ),
plugins: [
'@wordpress/theme/stylelint-plugins/no-unknown-ds-tokens',
'@wordpress/theme/stylelint-plugins/no-setting-wpds-custom-properties',
],
rules: {
'at-rule-empty-line-before': [
'always',
Expand Down Expand Up @@ -68,6 +72,8 @@ module.exports = {
'selector-pseudo-element-colon-notation': 'double',
'selector-type-case': 'lower',
'value-keyword-case': 'lower',
'plugin-wpds/no-setting-wpds-custom-properties': true,
'plugin-wpds/no-unknown-ds-tokens': true,

/* Disable new rules from stylelint-config-recommended 7 > 14 */
'function-no-unknown': null,
Expand Down
1 change: 1 addition & 0 deletions packages/stylelint-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
},
"dependencies": {
"@stylistic/stylelint-plugin": "^3.0.1",
"@wordpress/theme": "file:../theme",
"stylelint-config-recommended": "^14.0.1",
"stylelint-config-recommended-scss": "^14.1.0"
},
Expand Down
1 change: 1 addition & 0 deletions packages/theme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@

### New Features

- Added stylelint plugins for design token linting: `no-unknown-ds-tokens` to catch references to non-existent design tokens, and `no-setting-wpds-custom-properties` to prevent reassignments of design token variables ([#74226](https://github.com/WordPress/gutenberg/pull/74226)).
- Expose `ThemeProvider` TypeScript type from package. While the component is still experimental, this makes it easier to use TypeScript typings in your code, which would otherwise be inaccessible. ([#74011](https://github.com/WordPress/gutenberg/pull/74011))
54 changes: 54 additions & 0 deletions packages/theme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,60 @@ The files generated in this step will all be committed to the repo.

After the prebuild step, the package will be built into its final form via the repo's standard package build script.

## Stylelint Plugins

This package provides Stylelint plugins to help enforce consistent usage of design tokens. To use them, add the plugins to your Stylelint configuration:

```json
{
"plugins": [
"@wordpress/theme/stylelint-plugins/no-unknown-ds-tokens",
"@wordpress/theme/stylelint-plugins/no-setting-wpds-custom-properties"
],
"rules": {
"plugin-wpds/no-unknown-ds-tokens": true,
"plugin-wpds/no-setting-wpds-custom-properties": true
}
}
```

### `plugin-wpds/no-unknown-ds-tokens`

This rule reports an error when a CSS value references a `--wpds-*` custom property that is not a valid design token. This helps catch typos and ensures that only valid design tokens are used.

```css
/* ✗ Error: '--wpds-unknown-token' is not a valid Design System token */
.example {
color: var(--wpds-unknown-token);
}

/* ✓ OK */
.example {
color: var(--wpds-color-fg-content-neutral);
}
```

### `plugin-wpds/no-setting-wpds-custom-properties`

This rule reports an error when a CSS declaration sets (defines) a custom property in the `--wpds-*` namespace. The design system tokens should only be consumed, not defined or overridden in consuming code.

```css
/* ✗ Error: Do not set CSS custom properties using the Design System tokens namespace */
.example {
--wpds-my-token: red;
}

/* ✗ Error: Overriding existing tokens is also not allowed */
.example {
--wpds-color-fg-content-neutral: red;
}

/* ✓ OK */
.example {
--my-custom-token: red;
}
```

## Contributing to this package

This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects.
Expand Down
10 changes: 9 additions & 1 deletion packages/theme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
},
"./design-tokens.css": "./src/prebuilt/css/design-tokens.css",
"./design-tokens.js": "./src/prebuilt/js/design-tokens.js",
"./stylelint-plugins/no-unknown-ds-tokens": "./src/stylelint-plugins/no-unknown-ds-tokens.js",
"./stylelint-plugins/no-setting-wpds-custom-properties": "./src/stylelint-plugins/no-setting-wpds-custom-properties.js",
Comment on lines +43 to +44

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about simply having these plugins live in the stylelint-config package since the testing infrastructure is already there, but alas people need to be able to use these lint plugins regardless of whether they use @wordpress/stylelint-config.

Comment thread
ciampo marked this conversation as resolved.
Comment on lines +43 to +44

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I'm noticing as I'm resolving conflicts in #74129: Should these have been written as .ts files and we reference the package exports from the build directory?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just saw this. What would be your preference here? No strong opinion on my end. Could be simpler to just split out a separate package for the linters, even.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to unblock that work by reverting back to the src/ file, but that basically just restores how you'd implemented this. But that's a good indication of the complications we'll encounter and need to address if we switch to using TypeScript.

"./package.json": "./package.json"
},
"react-native": "src/index",
Expand All @@ -64,7 +66,13 @@
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react-dom": "^18.0.0",
"stylelint": "^16.8.2"
},
"peerDependenciesMeta": {
"stylelint": {
"optional": true
}
},
"publishConfig": {
"access": "public"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import stylelint from 'stylelint';

const {
createPlugin,
utils: { report, ruleMessages, validateOptions },
} = stylelint;

const ruleName = 'plugin-wpds/no-setting-wpds-custom-properties';

const messages = ruleMessages( ruleName, {
rejected: () =>
`Do not set CSS custom properties using the Design System tokens namespace (i.e. beginning with --wpds-*).`,
} );

/** @type {import('stylelint').Rule} */
const ruleFunction = ( primary ) => {
return ( root, result ) => {
const validOptions = validateOptions( result, ruleName, {
actual: primary,
possible: [ true ],
} );

if ( ! validOptions ) {
return;
}

root.walkDecls( ( ruleNode ) => {
const { prop } = ruleNode;
if ( prop.startsWith( '--wpds-' ) ) {
report( {
message: messages.rejected(),
node: ruleNode,
result,
ruleName,
} );
}
} );
};
};

ruleFunction.ruleName = ruleName;
ruleFunction.messages = messages;

/** @type {import('stylelint').Plugin} */
export default createPlugin( ruleName, ruleFunction );
101 changes: 101 additions & 0 deletions packages/theme/src/stylelint-plugins/no-unknown-ds-tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import stylelint from 'stylelint';
import tokenList from '../prebuilt/js/design-tokens.js';

const DS_TOKEN_PREFIX = 'wpds-';

/**
* Extracts all unique CSS custom properties (variables) from a given CSS value string,
* including those in fallback positions, optionally filtering by a specific prefix.
*
* @param {string} value - The CSS value string to search for variables.
* @param {string} [prefix=''] - Optional prefix to filter variables (e.g., 'wpds-').
* @return {Set<string>} A Set of unique matched CSS variable names (e.g., Set { '--wpds-token' }).
*
* @example
* extractCSSVariables(
* 'border: 1px solid var(--wpds-border-color, var(--wpds-border-fallback)); ' +
* 'color: var(--wpds-color-fg, black); ' +
* 'background: var(--unrelated-bg);',
* 'wpds'
* );
* // → Set { '--wpds-border-color', '--wpds-border-fallback', '--wpds-color-fg' }
*/
function extractCSSVariables( value, prefix = '' ) {
const regex = /--[\w-]+/g;
/** @type {Set<string>} */
const variables = new Set();

let match;
while ( ( match = regex.exec( value ) ) !== null ) {
const variableName = match[ 0 ];
if ( variableName.startsWith( `--${ prefix }` ) ) {
variables.add( variableName );
}
}

return variables;
}

const knownTokens = new Set( tokenList );
const wpdsTokensRegex = new RegExp( `[^\\w]--${ DS_TOKEN_PREFIX }`, 'i' );

const {
createPlugin,
utils: { report, ruleMessages, validateOptions },
} = stylelint;

const ruleName = 'plugin-wpds/no-unknown-ds-tokens';

const messages = ruleMessages( ruleName, {
rejected: ( tokenNames ) =>
`The following CSS variables are not valid Design System tokens: ${ tokenNames }`,
} );

/** @type {import('stylelint').Rule} */
const ruleFunction = ( primary ) => {
return ( root, result ) => {
const validOptions = validateOptions( result, ruleName, {
actual: primary,
possible: [ true ],
} );

if ( ! validOptions ) {
return;
}

root.walkDecls( ( ruleNode ) => {
const { value } = ruleNode;
// Early match for WPDS tokens to avoid unnecessary processing.
if ( wpdsTokensRegex.test( value ) ) {
const usedTokens = extractCSSVariables(
value,
DS_TOKEN_PREFIX
);
const unknownTokens = new Set(
[ ...usedTokens ].filter(
( token ) => ! knownTokens.has( token )
)
);

if ( unknownTokens.size > 0 ) {
report( {
message: messages.rejected(
Array.from( unknownTokens )
.map( ( token ) => `'${ token }'` )
.join( ', ' )
),
node: ruleNode,
result,
ruleName,
} );
}
}
} );
};
};

ruleFunction.ruleName = ruleName;
ruleFunction.messages = messages;

/** @type {import('stylelint').Plugin} */
export default createPlugin( ruleName, ruleFunction );
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This file needs to exist so test files have a way to unignore the files in this folder.
10 changes: 10 additions & 0 deletions packages/theme/src/stylelint-plugins/test/.stylelintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"plugins": [
"../no-unknown-ds-tokens.js",
"../no-setting-wpds-custom-properties.js"
],
"rules": {
"plugin-wpds/no-unknown-ds-tokens": true,
"plugin-wpds/no-setting-wpds-custom-properties": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`flags warnings with invalid wpds custom properties css snapshot matches warnings 1`] = `
[
{
"column": 2,
"endColumn": 31,
"endLine": 3,
"line": 3,
"rule": "plugin-wpds/no-setting-wpds-custom-properties",
"severity": "error",
"text": "Do not set CSS custom properties using the Design System tokens namespace (i.e. beginning with --wpds-*). (plugin-wpds/no-setting-wpds-custom-properties)",
},
{
"column": 2,
"endColumn": 27,
"endLine": 7,
"line": 7,
"rule": "plugin-wpds/no-setting-wpds-custom-properties",
"severity": "error",
"text": "Do not set CSS custom properties using the Design System tokens namespace (i.e. beginning with --wpds-*). (plugin-wpds/no-setting-wpds-custom-properties)",
},
]
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`flags warnings with invalid wpds tokens css snapshot matches warnings 1`] = `
[
{
"column": 2,
"endColumn": 39,
"endLine": 3,
"line": 3,
"rule": "plugin-wpds/no-unknown-ds-tokens",
"severity": "error",
"text": "The following CSS variables are not valid Design System tokens: '--wpds-nonexistent-token' (plugin-wpds/no-unknown-ds-tokens)",
},
{
"column": 2,
"endColumn": 40,
"endLine": 7,
"line": 7,
"rule": "plugin-wpds/no-unknown-ds-tokens",
"severity": "error",
"text": "The following CSS variables are not valid Design System tokens: '--wpds-made-up-color' (plugin-wpds/no-unknown-ds-tokens)",
},
]
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* Invalid: Setting wpds custom properties is not allowed */
:root {
--wpds-custom-override: #f00;
}

.example {
--wpds-local-token: #00f;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* Valid: Using wpds tokens as values is allowed */
.example {
color: var(--wpds-color-fg-content-neutral);
background-color: var(--wpds-color-bg-surface-neutral);
}

/* Valid: Setting non-wpds custom properties is allowed */
:root {
--my-custom-color: #f00;
--another-property: 10px;
}

Loading
Loading