UNPKG

@patreon/studio

Version:

Patreon Studio Design System

161 lines (134 loc) 5.07 kB
import valueParser from 'postcss-value-parser'; import stylelint from 'stylelint'; import { declarationValueIndex } from 'stylelint/lib/utils/nodeFieldIndices.mjs'; import { tokenAliases, tokenReferences, typographyTokenReferences } from './tokens.js'; const tokenReferencesWithAliases = { ...tokenReferences, ...tokenAliases }; const { createPlugin, utils: { report, ruleMessages, validateOptions }, } = stylelint; const reversedTokenReferences = Object.entries(tokenReferencesWithAliases).reduce((ret, [key, value]) => { ret[value] = key; return ret; }, {}); const ruleName = 'studio/token-validation'; const messages = ruleMessages(ruleName, { expected: (original, fixed) => `Expected "${fixed}" instead of "${original}" while using Studio tokens`, invalid: (original) => `"${original}" is not a valid Studio token`, }); /** @type {import('stylelint').Rule} */ function pluginFunction(primaryOption, _, context) { return (root, result) => { // validate the options const validOptions = validateOptions(result, ruleName, { actual: primaryOption, possible: [true, false], }); // bail if it is not a valid option if (!validOptions || !primaryOption) { return; } // walk all the declarations /** @param {Declaration} decl */ root.walkDecls((decl) => { const originalValue = decl.value; // Quick check: if there's no `var(` or `token(` in the value, bail if (!originalValue.includes('var(') && !originalValue.includes('token(')) { return; } let hasFix = false; // walk the parsed value const parsed = valueParser(originalValue); parsed.walk((node) => { if (node.type === 'function') { if (node.value === 'var') { const [varValueNode] = node.nodes; if (!varValueNode) { return; } const varValue = varValueNode.value; // we only care about global/component tokens if ( !(varValue.startsWith('--global') || varValue.startsWith('--component') || varValue.startsWith('--type')) ) { return; } // see if the token exists in our token reference map const referenceKey = `var(${varValue})`; const tokenValue = reversedTokenReferences[referenceKey]; // build some useful information for the error message const nodeString = valueParser.stringify(node); const index = declarationValueIndex(decl) + node.sourceIndex; const endIndex = index + nodeString.length; // if there is no token reference, we should report it as invalid if (!tokenValue) { report({ ruleName, result, node: decl, message: messages.invalid(nodeString), index, endIndex, }); } // if the token is defined, we should report it and provide a fix else { if (context.fix) { hasFix = true; node.value = 'token'; node.nodes = [ { type: 'string', quote: '"', value: tokenValue, }, ]; } else { report({ ruleName, result, node: decl, message: messages.expected(nodeString, `token("${tokenValue}")`), index, endIndex, }); } } } // check if tokens are valid if (node.value === 'token' || node.value === 'type-token') { const [tokenValueNode] = node.nodes; if (!tokenValueNode) { return; } const tokenValue = tokenValueNode.value; const reference = node.value === 'token' ? tokenReferencesWithAliases : typographyTokenReferences; const tokenReference = reference[tokenValue]; // if there is no token reference, we should report it as invalid if (!tokenReference) { const nodeString = valueParser.stringify(node); const index = declarationValueIndex(decl) + node.sourceIndex; const endIndex = index + nodeString.length; report({ ruleName, result, node: decl, message: messages.invalid(nodeString), index, endIndex, }); } } } }, true); // only update the declaration if there was a fix if (hasFix) { decl.value = parsed.toString(); } }); }; } pluginFunction.ruleName = ruleName; pluginFunction.messages = messages; // biome-ignore lint/style/noDefaultExport: legacy code export default createPlugin(ruleName, pluginFunction);