@atlaskit/stylelint-design-system
Version:
Stylelint plugin for use with the Atlassian Design System.
272 lines (259 loc) • 11.6 kB
JavaScript
"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;