Repository navigation
Stylelint: Add design token linting #74226
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bec4b17
8cef1d3
cceebe2
20161f2
7dc499e
eb162f3
ce0b960
b33a94d
33c3ead
e29f104
11c3d26
9ab1b09
dfab250
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,3 +2,4 @@ build | |
| build-style | ||
| node_modules | ||
| packages/stylelint-config/test | ||
| packages/theme/src/stylelint-plugins/test/fixtures | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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", | ||
|
ciampo marked this conversation as resolved.
Comment on lines
+43
to
+44
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was able to unblock that work by reverting back to the |
||
| "./package.json": "./package.json" | ||
| }, | ||
| "react-native": "src/index", | ||
|
|
@@ -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" | ||
|
|
||
| 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 ); |
| 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. |
| 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; | ||
| } | ||
|
|
There was a problem hiding this comment.
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-configpackage 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.