Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 3 additions & 1 deletion package-lock.json

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

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
5 changes: 4 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.mjs",
"./stylelint-plugins/no-setting-wpds-custom-properties": "./src/stylelint-plugins/no-setting-wpds-custom-properties.mjs",
"./package.json": "./package.json"
},
"react-native": "src/index",
Expand All @@ -64,7 +66,8 @@
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react-dom": "^18.0.0",
"stylelint": "^16.8.2"
},
"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( prop ),

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Check

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / check

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Run performance tests

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Playwright - 3

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Playwright - 4

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Playwright - 6

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Playwright - 5

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Playwright - 8

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Playwright - 1

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Playwright - 2

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Playwright - 7

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Build JavaScript assets for PHP unit tests

Expected 0 arguments, but got 1.

Check failure on line 31 in packages/theme/src/stylelint-plugins/no-setting-wpds-custom-properties.mjs

View workflow job for this annotation

GitHub Actions / Build Release Artifact

Expected 0 arguments, but got 1.
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.mjs
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;
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 (public) 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 );

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.mjs",
"../no-setting-wpds-custom-properties.mjs"
],
"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 (public) 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 (public) 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;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* Invalid: Using unknown design system tokens */
.example {
color: var(--wpds-nonexistent-token);
}

.another {
background: var(--wpds-made-up-color);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* Valid: Using known design system tokens */
.example {
background-color: var(--wpds-color-bg-surface-neutral);
color: var(--wpds-color-fg-content-neutral);
border-radius: var(--wpds-border-radius-surface-sm);
padding: var(--wpds-dimension-padding-surface-md);
}

/* Valid: Using tokens with fallbacks */
.with-fallback {
color: var(--wpds-color-fg-content-neutral, inherit);
}

/* Valid: Non-wpds custom properties are ignored */
.other-vars {
color: var(--my-custom-color);
background: var(--some-other-var);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const utils = require( './utils' );
const getStylelintResult = utils.getStylelintResult;

describe( 'flags no warnings with valid wpds custom properties css', () => {
let result;

beforeEach( () => {
result = getStylelintResult(
'./fixtures/no-setting-wpds-custom-properties-valid.css'
);
} );

it( 'did not error', () => {
return result.then( ( data ) => expect( data.errored ).toBeFalsy() );
} );

it( 'flags no warnings', () => {
return result.then( ( data ) =>
expect( data.results[ 0 ].warnings ).toHaveLength( 0 )
);
} );
} );

describe( 'flags warnings with invalid wpds custom properties css', () => {
let result;

Check failure on line 26 in packages/theme/src/stylelint-plugins/test/no-setting-wpds-custom-properties.js

View workflow job for this annotation

GitHub Actions / JavaScript (Node.js 20) 2/4

SyntaxError: Unexpected token 'E', "Error: All"... is not valid JSON at JSON.parse (<anonymous>) at parse (/home/runner/work/gutenberg/gutenberg/packages/theme/src/stylelint-plugins/test/utils/index.js:26:20) at processTicksAndRejections (node:internal/process/task_queues:95:5)

Check failure on line 26 in packages/theme/src/stylelint-plugins/test/no-setting-wpds-custom-properties.js

View workflow job for this annotation

GitHub Actions / JavaScript (Node.js 20) 2/4

SyntaxError: Unexpected token 'E', "Error: All"... is not valid JSON at JSON.parse (<anonymous>) at parse (/home/runner/work/gutenberg/gutenberg/packages/theme/src/stylelint-plugins/test/utils/index.js:26:20) at processTicksAndRejections (node:internal/process/task_queues:95:5)

Check failure on line 26 in packages/theme/src/stylelint-plugins/test/no-setting-wpds-custom-properties.js

View workflow job for this annotation

GitHub Actions / JavaScript (Node.js 20) 2/4

SyntaxError: Unexpected token 'E', "Error: All"... is not valid JSON at JSON.parse (<anonymous>) at parse (/home/runner/work/gutenberg/gutenberg/packages/theme/src/stylelint-plugins/test/utils/index.js:26:20) at processTicksAndRejections (node:internal/process/task_queues:95:5)

Check failure on line 26 in packages/theme/src/stylelint-plugins/test/no-setting-wpds-custom-properties.js

View workflow job for this annotation

GitHub Actions / JavaScript (Node.js 20) 2/4

SyntaxError: Unexpected token 'E', "Error: All"... is not valid JSON at JSON.parse (<anonymous>) at parse (/home/runner/work/gutenberg/gutenberg/packages/theme/src/stylelint-plugins/test/utils/index.js:26:20) at processTicksAndRejections (node:internal/process/task_queues:95:5)

Check failure on line 26 in packages/theme/src/stylelint-plugins/test/no-setting-wpds-custom-properties.js

View workflow job for this annotation

GitHub Actions / JavaScript (Node.js 20) 2/4

SyntaxError: Unexpected token 'E', "Error: All"... is not valid JSON at JSON.parse (<anonymous>) at parse (/home/runner/work/gutenberg/gutenberg/packages/theme/src/stylelint-plugins/test/utils/index.js:26:20) at processTicksAndRejections (node:internal/process/task_queues:95:5)
beforeEach( () => {
result = getStylelintResult(
'./fixtures/no-setting-wpds-custom-properties-invalid.css'
);
} );

it( 'did error', () => {
return result.then( ( data ) => expect( data.errored ).toBeTruthy() );
} );

it( 'flags correct number of warnings', () => {
return result.then( ( data ) =>
expect( data.results[ 0 ].warnings ).toHaveLength( 2 )
);
} );

it( 'snapshot matches warnings', () => {
return result.then( ( data ) =>
expect( data.results[ 0 ].warnings ).toMatchSnapshot()
);
} );
} );
Loading
Loading