-
Notifications
You must be signed in to change notification settings - Fork 4.8k
ESLint: Add bare token check to no-unknown-ds-tokens
#76210
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
378fe60
f00e0ba
202fdd9
8aca019
ec82b42
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 |
|---|---|---|
|
|
@@ -4,35 +4,41 @@ const tokenList = tokenListModule.default || tokenListModule; | |
| 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. | ||
| * Single-pass extraction that finds all `--prefix-*` tokens in a CSS value | ||
| * string and classifies each occurrence as `var()`-wrapped or bare. | ||
| * | ||
| * @param {string} value - The CSS value string to search for variables. | ||
| * @param {string} value - The CSS value string to search. | ||
| * @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' }). | ||
| * @return {{ tokens: Set<string>, bare: Set<string> }} | ||
| * `tokens` — every unique matched token; | ||
| * `bare` — the subset that appeared at least once without a `var()` wrapper. | ||
| * | ||
| * @example | ||
| * extractCSSVariables( | ||
| * 'border: 1px solid var(--wpds-border-color, var(--wpds-border-fallback)); ' + | ||
| * 'color: var(--wpds-color-fg, black); ' + | ||
| * 'background: var(--unrelated-bg);', | ||
| * 'wpds' | ||
| * classifyTokens( | ||
| * 'var(--wpds-color-fg) --wpds-color-bg', | ||
| * 'wpds-' | ||
| * ); | ||
| * // → Set { '--wpds-border-color', '--wpds-border-fallback', '--wpds-color-fg' } | ||
| * // → { tokens: Set {'--wpds-color-fg','--wpds-color-bg'}, | ||
| * // bare: Set {'--wpds-color-bg'} } | ||
| */ | ||
| function extractCSSVariables( value, prefix = '' ) { | ||
| const regex = /--[\w-]+/g; | ||
| const variables = new Set(); | ||
| function classifyTokens( value, prefix = '' ) { | ||
| const regex = new RegExp( | ||
| `(?:^|[^\\w])(var\\(\\s*)?(--${ prefix }[\\w-]+)`, | ||
| 'g' | ||
| ); | ||
| const tokens = new Set(); | ||
| const bare = new Set(); | ||
|
|
||
| let match; | ||
| while ( ( match = regex.exec( value ) ) !== null ) { | ||
| const variableName = match[ 0 ]; | ||
| if ( variableName.startsWith( `--${ prefix }` ) ) { | ||
| variables.add( variableName ); | ||
| const token = match[ 2 ]; | ||
| tokens.add( token ); | ||
| if ( ! match[ 1 ] ) { | ||
| bare.add( token ); | ||
| } | ||
| } | ||
|
|
||
| return variables; | ||
| return { tokens, bare }; | ||
| } | ||
|
|
||
| const knownTokens = new Set( tokenList ); | ||
|
|
@@ -50,6 +56,8 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( { | |
| 'The following CSS variables are not valid Design System tokens: {{ tokenNames }}', | ||
| dynamicToken: | ||
| 'Design System tokens must not be dynamically constructed, as they cannot be statically verified for correctness or processed automatically to inject fallbacks.', | ||
| bareToken: | ||
| 'Design System tokens must be wrapped in `var()` for build-time fallback injection to work: {{ tokenNames }}', | ||
| }, | ||
| }, | ||
| create( context ) { | ||
|
|
@@ -71,6 +79,7 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( { | |
| [ dynamicTemplateLiteralAST ]( node ) { | ||
| let hasDynamic = false; | ||
| const unknownTokens = []; | ||
| const bareTokens = []; | ||
|
|
||
| for ( const quasi of node.quasis ) { | ||
| const raw = quasi.value.raw; | ||
|
|
@@ -84,7 +93,7 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( { | |
| hasDynamic = true; | ||
| } | ||
|
|
||
| const tokens = extractCSSVariables( | ||
| const { tokens, bare } = classifyTokens( | ||
| value, | ||
| DS_TOKEN_PREFIX | ||
| ); | ||
|
|
@@ -95,12 +104,15 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( { | |
| const endMatch = value.match( /(--([\w-]+))$/ ); | ||
| if ( endMatch ) { | ||
| tokens.delete( endMatch[ 1 ] ); | ||
| bare.delete( endMatch[ 1 ] ); | ||
| } | ||
| } | ||
|
|
||
| for ( const token of tokens ) { | ||
| if ( ! knownTokens.has( token ) ) { | ||
| unknownTokens.push( token ); | ||
| } else if ( bare.has( token ) ) { | ||
| bareTokens.push( token ); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
79
to
118
|
||
|
|
@@ -123,6 +135,18 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( { | |
| }, | ||
| } ); | ||
| } | ||
|
|
||
| if ( bareTokens.length > 0 ) { | ||
| context.report( { | ||
| node, | ||
| messageId: 'bareToken', | ||
| data: { | ||
| tokenNames: bareTokens | ||
| .map( ( token ) => `'${ token }'` ) | ||
| .join( ', ' ), | ||
| }, | ||
| } ); | ||
| } | ||
| }, | ||
| /** @param {import('estree').Literal | import('estree').TemplateElement} node */ | ||
| [ staticTokensAST ]( node ) { | ||
|
|
@@ -145,7 +169,7 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( { | |
| return; | ||
| } | ||
|
|
||
| const usedTokens = extractCSSVariables( | ||
| const { tokens: usedTokens, bare } = classifyTokens( | ||
| computedValue, | ||
| DS_TOKEN_PREFIX | ||
| ); | ||
|
|
@@ -164,6 +188,31 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( { | |
| }, | ||
| } ); | ||
| } | ||
|
|
||
| // Skip bare-token check for property keys | ||
| // (e.g. `{ '--wpds-token': value }` declaring a custom property). | ||
| const isPropertyKey = | ||
| node.parent?.type === 'Property' && | ||
| node.parent.key === node; | ||
|
|
||
| if ( ! isPropertyKey ) { | ||
| const bareTokens = [ ...usedTokens ].filter( | ||
| ( token ) => | ||
| knownTokens.has( token ) && bare.has( token ) | ||
| ); | ||
|
|
||
| if ( bareTokens.length > 0 ) { | ||
| context.report( { | ||
| node, | ||
| messageId: 'bareToken', | ||
| data: { | ||
| tokenNames: bareTokens | ||
| .map( ( token ) => `'${ token }'` ) | ||
| .join( ', ' ), | ||
| }, | ||
| } ); | ||
| } | ||
| } | ||
| }, | ||
| }; | ||
| }, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.