UNPKG

@atlaskit/stylelint-design-system

Version:

Stylelint plugin for use with the Atlassian Design System.

272 lines (259 loc) 11.6 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.ruleName = exports.messages = exports.default = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _postcssValueParser = _interopRequireDefault(require("postcss-value-parser")); var _stylelint = _interopRequireDefault(require("stylelint")); var _getSpacingToken = require("../../utils/get-spacing-token"); var _isColorFunction = require("../../utils/is-color-function"); var _isFunction = require("../../utils/is-function"); var _isHexColor = require("../../utils/is-hex-color"); var _isLengthOrPercentage = require("../../utils/is-length-or-percentage"); var _isNamedColor = require("../../utils/is-named-color"); var _isSpacingRule = require("../../utils/is-spacing-rule"); var _isToken = require("../../utils/is-token"); var _isTypographyRule = require("../../utils/is-typography-rule"); var _isVar = require("../../utils/is-var"); 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) { (0, _defineProperty2.default)(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; } var defaultIsEnabled = { color: true, spacing: false, typography: false, nonTokenCssVariables: false }; var ruleName = exports.ruleName = 'design-system/ensure-design-token-usage'; var tokenUrl = 'https://atlassian.design/components/tokens/examples'; var messages = exports.messages = _stylelint.default.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 (0, _isColorFunction.isColorFunction)(node.value); case 'word': return (0, _isHexColor.isHexColor)(node.value) || (0, _isNamedColor.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.default.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) { (0, _postcssValueParser.default)(decl.value).walk(function (node) { if (isEnabled.color) { if ((0, _isFunction.isFunction)(node) && (0, _isVar.isVar)(node)) { if ((0, _isToken.isToken)(node.nodes[0])) { return false; } if (isEnabled.nonTokenCssVariables && !(0, _isToken.isToken)(node.nodes[0])) { /** * If we find a var, ensure it's a token var */ _stylelint.default.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.default.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 ((0, _isSpacingRule.isSpacingRule)(decl.prop)) { if ((0, _isFunction.isFunction)(node) && (0, _isVar.isVar)(node)) { // A valid token was used, exit if ((0, _isToken.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.default.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 ((0, _isLengthOrPercentage.isLengthOrPercentage)(node.value)) { // Check if we can auto-fix this spacing value by mapping it to a spacing token var spacingToken = (0, _getSpacingToken.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.default.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 ((0, _isTypographyRule.isTypographyRule)(decl.prop)) { if ((0, _isFunction.isFunction)(node) && (0, _isVar.isVar)(node)) { // A valid token was used, exit if ((0, _isToken.isToken)(node.nodes[0])) { return false; } // A variable that isn't a token was used in a spacing rule _stylelint.default.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 ((0, _isLengthOrPercentage.isLengthOrPercentage)(node.value)) { _stylelint.default.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.default.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 = (0, _postcssValueParser.default)(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.default.createPlugin(ruleName, rule); // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports var _default = exports.default = plugin;