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