@patreon/studio
Version:
Patreon Studio Design System
161 lines (134 loc) • 5.07 kB
JavaScript
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);