UNPKG

@atlaskit/stylelint-design-system

Version:

Stylelint plugin for use with the Atlassian Design System.

265 lines (253 loc) • 10.8 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import valueParser from 'postcss-value-parser'; import stylelint from 'stylelint'; import { getSpacingToken } from '../../utils/get-spacing-token'; import { isColorFunction } from '../../utils/is-color-function'; import { isFunction } from '../../utils/is-function'; import { isHexColor } from '../../utils/is-hex-color'; import { isLengthOrPercentage } from '../../utils/is-length-or-percentage'; import { isNamedColor } from '../../utils/is-named-color'; import { isSpacingRule } from '../../utils/is-spacing-rule'; import { isToken } from '../../utils/is-token'; import { isTypographyRule } from '../../utils/is-typography-rule'; import { isVar } from '../../utils/is-var'; var defaultIsEnabled = { color: true, spacing: false, typography: false, nonTokenCssVariables: false }; export var ruleName = 'design-system/ensure-design-token-usage'; var tokenUrl = 'https://atlassian.design/components/tokens/examples'; export var messages = stylelint.utils.ruleMessages(ruleName, { noHardcodedColors: "Color values should be design tokens. See ".concat(tokenUrl, " for guidance."), noHardcodedSpacing: "Spacing values should be design tokens. See ".concat(tokenUrl, " for guidance."), noHardcodedTypography: "Typography values should be design tokens. See ".concat(tokenUrl, " for guidance."), noNonTokenVars: 'CSS variables should be wrapped in a design token.' }); var isColorNode = function isColorNode(node) { switch (node.type) { case 'function': return isColorFunction(node.value); case 'word': return isHexColor(node.value) || isNamedColor(node.value); } }; var ruleBase = function ruleBase() { var isEnabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultIsEnabled; var _secondaryOptions = arguments.length > 1 ? arguments[1] : undefined; var context = arguments.length > 2 ? arguments[2] : undefined; return function (root, result) { // Map to store spacing values for each declaration during fix mode // This allows us to collect all spacing values before applying fixes // to avoid position shifting issues when modifying the AST var declarationSpacingValues = new Map(); var validOptions = stylelint.utils.validateOptions(result, ruleName, { actual: isEnabled, possible: { color: [true, false], spacing: [true, false], typography: [true, false], nonTokenCssVariables: [true, false] } }); if (!validOptions) { return; } root.walkDecls(function (decl) { valueParser(decl.value).walk(function (node) { if (isEnabled.color) { if (isFunction(node) && isVar(node)) { if (isToken(node.nodes[0])) { return false; } if (isEnabled.nonTokenCssVariables && !isToken(node.nodes[0])) { /** * If we find a var, ensure it's a token var */ stylelint.utils.report({ message: messages.noNonTokenVars, node: decl, word: node.value, result: result, ruleName: ruleName }); return false; } } /** * If we find a color function (rgba, hsl) or color (#eee), ensure it's a token */ if (isColorNode(node)) { stylelint.utils.report({ message: messages.noHardcodedColors, node: decl, word: node.value, result: result, ruleName: ruleName }); return false; } } if (isEnabled.spacing) { // Rule is gap, margin, padding, etc if (isSpacingRule(decl.prop)) { if (isFunction(node) && isVar(node)) { // A valid token was used, exit if (isToken(node.nodes[0])) { return false; } // A variable that isn't a token was used in a spacing rule // Only report in lint mode, not fix mode to avoid duplicate reports if (!context.fix) { stylelint.utils.report({ message: messages.noHardcodedSpacing, node: decl, word: node.value, result: result, ruleName: ruleName }); } return false; } /** * Report on px, cm, in, etc * This is necessary because we walk multiple types of nodes. * So we need to first check whether it's a value that's a length * or percentage so we don't report on other types of nodes like * 'prop'. */ if (isLengthOrPercentage(node.value)) { // Check if we can auto-fix this spacing value by mapping it to a spacing token var spacingToken = getSpacingToken(node.value); // In fix mode, handle differently to collect values for post-processing if (context.fix) { // Store all spacing values for post-processing to avoid position shifting if (!declarationSpacingValues.has(decl)) { declarationSpacingValues.set(decl, []); } declarationSpacingValues.get(decl).push({ originalValue: node.value, spacingToken: spacingToken, shouldReport: !spacingToken, originalNode: _objectSpread({}, node) // Store a copy of the node for later use }); return false; // Don't report yet - handle in post-processing } else { // In lint mode (not fix mode), report errors immediately stylelint.utils.report({ message: messages.noHardcodedSpacing, node: decl, word: node.value, result: result, ruleName: ruleName }); return false; } } } } if (isEnabled.typography) { // Rule is font-size, line-height, etc if (isTypographyRule(decl.prop)) { if (isFunction(node) && isVar(node)) { // A valid token was used, exit if (isToken(node.nodes[0])) { return false; } // A variable that isn't a token was used in a spacing rule stylelint.utils.report({ message: messages.noHardcodedTypography, node: decl, word: node.value, result: result, ruleName: ruleName }); return false; } /** * Report on px, cm, in, etc * This is necessary because we walk multiple types of nodes. * So we need to first check whether it's a value that's a length * or percentage so we don't report on other types of nodes like * 'prop'. */ if (isLengthOrPercentage(node.value)) { stylelint.utils.report({ message: messages.noHardcodedTypography, node: decl, word: node.value, result: result, ruleName: ruleName }); return false; } } } }); }); // Post-process spacing values in fix mode // This section handles the actual auto-fixing of spacing values to design tokens if (context.fix) { declarationSpacingValues.forEach(function (spacingValues, decl) { // First, report errors using original positions before any modifications // This ensures error messages are accurate even after fixes are applied var reportableValues = spacingValues.filter(function (sv) { return sv.shouldReport; }); reportableValues.forEach(function (spacingValue) { stylelint.utils.report({ message: messages.noHardcodedSpacing, node: decl, word: spacingValue.originalValue, result: result, ruleName: ruleName }); }); // Then apply fixes from right to left to avoid position shifting var parsedValue = valueParser(decl.value); // Collect positions for fixing by walking the parsed value again var valuePositions = []; parsedValue.walk(function (node) { if (node.type === 'word') { // Find matching spacing values that have valid tokens for replacement var matchingSpacingValue = spacingValues.find(function (sv) { return sv.originalValue === node.value && sv.spacingToken; }); if (matchingSpacingValue) { valuePositions.push({ node: node, spacingValue: matchingSpacingValue, sourceIndex: node.sourceIndex || 0 }); } } }); // Sort by position (highest first = right to left) and apply fixes // This prevents position shifting issues when modifying multiple values valuePositions.sort(function (a, b) { return b.sourceIndex - a.sourceIndex; }).forEach(function (_ref) { var node = _ref.node, spacingValue = _ref.spacingValue; node.value = spacingValue.spacingToken; }); // Update the declaration value with all fixes applied decl.value = parsedValue.toString(); }); } }; }; var rule = Object.assign(ruleBase, { ruleName: ruleName, messages: messages, meta: { url: tokenUrl, fixable: true } }); var plugin = stylelint.createPlugin(ruleName, rule); // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports export default plugin;