Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
import stylelint from "stylelint";
import valueParser from "postcss-value-parser";
import {
namespace,
createTokenNamesArray,
createRawValuesObject,
isValidTokenUsage,
isValidTokenUsageInCalc,
containsViewportUnit,
getLocalCustomProperties,
usesRawFallbackValues,
usesRawShorthandValues,
createAllowList,
FIXED_UNITS,
} from "../helpers.mjs";
const {
utils: { report, ruleMessages, validateOptions },
} = stylelint;
const ruleName = namespace("use-size-tokens");
const messages = ruleMessages(ruleName, {
rejected: value =>
`Consider using a size design token instead of ${value}. This may be fixable by running the same command again with --fix.`,
});
const meta = {
fixable: true,
};
const SIZE = {
CATEGORIES: ["size", "icon-size"],
PROPERTIES: [
"width",
"min-width",
"max-width",
"height",
"min-height",
"max-height",
"inline-size",
"min-inline-size",
"max-inline-size",
"block-size",
"min-block-size",
"max-block-size",
"inset",
"inset-block",
"inset-block-end",
"inset-block-start",
"inset-inline",
"inset-inline-end",
"inset-inline-start",
"left",
"right",
"top",
"bottom",
"background-size",
],
};
const tokenCSS = createTokenNamesArray(SIZE.CATEGORIES);
// Allowed size values in CSS
const SIZE_TOKENS_ALLOW_LIST = createAllowList([
FIXED_UNITS,
"0",
"auto",
"none",
"fit-content",
"min-content",
"max-content",
]);
const RAW_VALUE_TO_TOKEN_VALUE = {
...createRawValuesObject(SIZE.CATEGORIES),
"0.75rem": "var(--size-item-xsmall)",
"1rem": "var(--size-item-small)",
"1.5rem": "var(--size-item-medium)",
"2rem": "var(--size-item-large)",
"3rem": "var(--size-item-xlarge)",
};
const ruleFunction = primaryOption => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primaryOption,
possible: [true],
});
if (!validOptions) {
return;
}
// Walk declarations once to generate a lookup table of variables.
const cssCustomProperties = getLocalCustomProperties(root);
// Walk declarations again to detect non-token values.
root.walkDecls(declarations => {
if (!SIZE.PROPERTIES.includes(declarations.prop)) {
return;
}
// Allows values using `vh` or `vw` units
if (containsViewportUnit(declarations.value)) {
return;
}
// Otherwise, see if we are using the tokens correctly
if (
isValidTokenUsage(
declarations.value,
tokenCSS,
cssCustomProperties,
SIZE_TOKENS_ALLOW_LIST
) &&
isValidTokenUsageInCalc(
declarations.value,
tokenCSS,
cssCustomProperties,
SIZE_TOKENS_ALLOW_LIST
) &&
!usesRawFallbackValues(declarations.value, RAW_VALUE_TO_TOKEN_VALUE) &&
!usesRawShorthandValues(
declarations.value,
tokenCSS,
cssCustomProperties,
SIZE_TOKENS_ALLOW_LIST
)
) {
return;
}
report({
message: messages.rejected(declarations.value),
node: declarations,
result,
ruleName,
fix: () => {
const val = valueParser(declarations.value);
let hasFixes = false;
val.walk(node => {
if (node.type === "word") {
const token = RAW_VALUE_TO_TOKEN_VALUE[node.value.trim()];
if (token) {
hasFixes = true;
node.value = token;
}
}
});
if (hasFixes) {
declarations.value = val.toString();
}
},
});
});
};
};
ruleFunction.ruleName = ruleName;
ruleFunction.messages = messages;
ruleFunction.meta = meta;
export default ruleFunction;