@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
271 lines (253 loc) • 13.5 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; }
/* eslint-disable @repo/internal/react/require-jsdoc */
import { isNodeOfType } from 'eslint-codemod-utils';
import { getSourceCode } from '@atlaskit/eslint-utils/context-compat';
import { Object as ASTObject } from '../../../ast-nodes/object';
import { ObjectEntry } from '../../../ast-nodes/object-entry';
import { Root } from '../../../ast-nodes/root';
import { getValueForPropertyNode } from '../../ensure-design-token-usage/get-value-for-property-node';
import { normaliseValue } from '../../ensure-design-token-usage/normalise-value';
import { isDecendantOfGlobalToken } from '../../utils/is-decendant-of-global-token';
import { isDecendantOfStyleBlock } from '../../utils/is-decendant-of-style-block';
import { isDecendantOfType } from '../../utils/is-decendant-of-type';
import { convertPropertyNodeToStringableNode } from '../convert-property-node-to-stringable-node';
import { defaultFontWeight } from '../default-font-weight';
import { findFontWeightTokenForValue } from '../find-font-weight-token-for-value';
import { findTypographyTokenForValues } from '../find-typography-token-for-values';
import { fontWeightMap } from '../font-weight-map';
import { getLiteralProperty } from '../get-literal-property';
import { getTokenProperty } from '../get-token-property';
import { insertTokensImport } from '../insert-tokens-import';
import { isValidPropertyNode } from '../is-valid-property-node';
import { isValidTypographyToken } from '../is-valid-typography-token';
import { notUndefined } from '../not-undefined';
export var StyleObject = {
lint: function lint(node, _ref) {
var context = _ref.context,
config = _ref.config;
// To force the correct node type
if (!isNodeOfType(node, 'ObjectExpression')) {
return {
success: false
};
}
// Check whether all criteria needed to make a transformation are met
var _StyleObject$_check = StyleObject._check(node, {
context: context,
config: config
}),
success = _StyleObject$_check.success,
refs = _StyleObject$_check.refs;
if (!success || !refs) {
return;
}
var fontSizeNode = refs.fontSizeNode,
fontSizeRaw = refs.fontSizeRaw,
tokensImportNode = refs.tokensImportNode;
var fontSizeValue = normaliseValue('fontSize', fontSizeRaw);
// -- Font weight --
var fontWeightNode = ASTObject.getEntryByPropertyName(node, 'fontWeight');
var fontWeightRaw = fontWeightNode && getValueForPropertyNode(fontWeightNode, context);
// If fontWeightRaw is a token we find the token name and treat it like a raw value for simplicity.
// e.g. token('font.weight.bold', '700') ends up as '700' after this if-block.
// That way the token matching logic still runs and the font weight declaration can be removed and re-added after the main font token.
if (fontWeightRaw && typeof fontWeightRaw === 'string' && fontWeightRaw.includes('font.weight.')) {
var _fontWeightRaw$match;
var fontWeightTokenSuffix = ((_fontWeightRaw$match = fontWeightRaw.match(/font\.weight\.(\w*)/)) === null || _fontWeightRaw$match === void 0 ? void 0 : _fontWeightRaw$match[1]) || 'regular'; // ${token('font.weight.bold', '700')} -> 'bold'
if (Object.keys(fontWeightMap).includes(fontWeightTokenSuffix)) {
fontWeightRaw = fontWeightMap[fontWeightTokenSuffix];
}
}
// If no fontWeight value exists, default to 400 to avoid matching with a bolder token resulting in a visual change
var fontWeightValue = fontWeightRaw && normaliseValue('fontWeight', fontWeightRaw) || defaultFontWeight;
fontWeightValue = fontWeightValue.length === 3 ? fontWeightValue : fontWeightMap[fontWeightValue] || defaultFontWeight;
// -- Line height --
var lineHeightNode = ASTObject.getEntryByPropertyName(node, 'lineHeight');
var lineHeightRaw = lineHeightNode && getValueForPropertyNode(lineHeightNode, context);
var shouldAddFontWeight = false;
var lineHeightValue = lineHeightRaw && normaliseValue('lineHeight', lineHeightRaw) || undefined;
if (lineHeightValue === fontSizeValue) {
lineHeightValue = '1';
}
// -- Match tokens --
// Check if fontSize is a token (this is invalid syntax but unfortunately a common occurence)
// We may as well auto-fix `fontSize` to `font` and keep the token.
// Other tokens like `fontSize: token('space.100')` will not autofix, but still report
var matchingTokens = [];
var isFontSizeAToken = isDecendantOfGlobalToken(fontSizeNode.value);
if (isFontSizeAToken) {
// Specifically match for valid, non-deprecated font.heading|body|code tokens
var match = fontSizeValue.match(/font.(body|heading|code)[^']*/);
if (match) {
var matchedTokenName = match[0];
// This is really just a double check to be 100% certain the token exists
// and that we're not trying to apply a deprecated fontSize token to the font property
if (isValidTypographyToken(matchedTokenName)) {
matchingTokens = [{
tokenName: matchedTokenName
}];
}
}
} else {
// Standard matching against fontSize/lineHeight values
matchingTokens = findTypographyTokenForValues(fontSizeValue, lineHeightValue);
if (matchingTokens.length) {
// If we have multiple matching tokens, try matching fontWeight
var matchingTokenWithWeight = findFontWeightTokenForValue(fontWeightValue, matchingTokens);
if (matchingTokenWithWeight) {
// Possibly narrowed down tokens
matchingTokens = [matchingTokenWithWeight];
} else {
// Ended up with 0 matches by matching fontWeight
// return body token and add fontWeight manually
matchingTokens = matchingTokens.filter(function (token) {
return token.tokenName.includes('.body');
});
shouldAddFontWeight = true;
}
}
}
// Get other font-* nodes that we can replace/remove.
// These aren't needed for token matching.
// -- Font family --
var fontFamilyNode = ASTObject.getEntryByPropertyName(node, 'fontFamily');
var fontFamilyRaw = fontFamilyNode && getValueForPropertyNode(fontFamilyNode, context);
var fontFamilyValue = fontFamilyRaw && normaliseValue('fontFamily', fontFamilyRaw) || undefined;
// If font family is already a token, we remove and re-add it
// Only need to do this for non-default font stacks as the defaults can be safely removed
if (fontFamilyValue && fontFamilyValue.includes('font.family.') && !(fontFamilyValue.includes('font.family.heading') || fontFamilyValue.includes('font.family.body'))) {
fontFamilyValue = undefined;
}
var fontFamilyToAdd;
// If font family uses the Charlie font we can't replace; exit
if (fontFamilyValue) {
if (fontFamilyValue.toLowerCase().includes('charlie display')) {
fontFamilyToAdd = 'heading';
} else if (fontFamilyValue.toLowerCase().includes('charlie text')) {
fontFamilyToAdd = 'body';
}
} else {
// Font family node exists but we can't resolve its value
// Will need to re-add it below the font property to ensure it still applies
fontFamilyToAdd = fontFamilyNode ? 'original' : undefined;
}
// -- Font style --
var fontStyleNode = ASTObject.getEntryByPropertyName(node, 'fontStyle');
var fontStyleRaw = fontStyleNode && getValueForPropertyNode(fontStyleNode, context);
var fontStyleValue = fontStyleRaw && normaliseValue('fontStyle', fontStyleRaw) || undefined;
var fontStyleToAdd;
if (fontStyleValue === 'italic') {
fontStyleToAdd = 'italic';
}
// -- Letter spacing --
var letterSpacingNode = ASTObject.getEntryByPropertyName(node, 'letterSpacing');
// A single matching token
// TOOD: Maybe suggest options if > 1 matching token
if (matchingTokens.length === 1) {
var matchingToken = matchingTokens[0];
// fontSize node is always first
var nodesToReplace = [fontSizeNode, fontWeightNode, lineHeightNode, fontFamilyNode, fontStyleNode, letterSpacingNode].filter(notUndefined);
var fontFamilyTokenName = fontFamilyToAdd ? "font.family.brand.".concat(fontFamilyToAdd) : '';
var fontWeightReplacementToken = shouldAddFontWeight ? findFontWeightTokenForValue(fontWeightValue) : undefined;
var fontWeightReplacement = fontWeightReplacementToken && getTokenProperty('fontWeight', fontWeightReplacementToken.tokenName);
var fontFamilyReplacement = fontFamilyToAdd && (fontFamilyToAdd === 'original' ? convertPropertyNodeToStringableNode(
// This will always exist if fontFamilyToAdd === 'original', TS can't figure that out.
fontFamilyNode) : getTokenProperty('fontFamily', fontFamilyTokenName));
var fontStyleReplacement = fontStyleToAdd && getLiteralProperty('fontStyle', fontStyleToAdd);
var fixerRefs = {
matchingToken: matchingToken,
nodesToReplace: nodesToReplace,
tokensImportNode: tokensImportNode,
fontWeightReplacement: fontWeightReplacement,
fontFamilyReplacement: fontFamilyReplacement,
fontStyleReplacement: fontStyleReplacement
};
var fix = StyleObject._fix(fixerRefs, context);
context.report(_objectSpread({
node: fontSizeNode,
messageId: isFontSizeAToken ? 'noFontSizeTypographyToken' : 'noRawTypographyValues'
}, config.enableUnsafeAutofix ? {
fix: fix
} : {
suggest: [{
desc: "Convert to font token",
fix: fix
}]
}));
} else if (!matchingTokens.length) {
context.report({
node: fontSizeNode,
messageId: 'noRawTypographyValues'
});
}
return;
},
_check: function _check(node, _ref2) {
var context = _ref2.context,
config = _ref2.config;
if (!config.patterns.includes('style-object')) {
return {
success: false
};
}
if (!isDecendantOfStyleBlock(node) && !isDecendantOfType(node, 'JSXExpressionContainer')) {
return {
success: false
};
}
// -- Font size --
var fontSizeNode = ASTObject.getEntryByPropertyName(node, 'fontSize');
if (!fontSizeNode || !isValidPropertyNode(fontSizeNode)) {
return {
success: false
};
}
var fontSizeRaw = getValueForPropertyNode(fontSizeNode, context);
// Without a valid fontSize value we can't be certain what token should be used; exit
if (fontSizeRaw === undefined || fontSizeRaw === null) {
return {
success: false
};
}
var tokensImportDeclaration = Root.findImportsByModule(getSourceCode(context).ast.body, '@atlaskit/tokens');
// If there is more than one `@atlaskit/tokens` import, then it becomes difficult to determine which import to transform
if (tokensImportDeclaration.length > 1) {
return {
success: false
};
}
return {
success: true,
refs: {
fontSizeNode: fontSizeNode,
fontSizeRaw: fontSizeRaw,
tokensImportNode: tokensImportDeclaration[0]
}
};
},
_fix: function _fix(refs, context) {
return function (fixer) {
var matchingToken = refs.matchingToken,
nodesToReplace = refs.nodesToReplace,
tokensImportNode = refs.tokensImportNode,
fontWeightReplacement = refs.fontWeightReplacement,
fontFamilyReplacement = refs.fontFamilyReplacement,
fontStyleReplacement = refs.fontStyleReplacement;
var fontSizeNode = nodesToReplace[0];
var root = getSourceCode(context).ast.body;
return (!tokensImportNode ? [insertTokensImport(root, fixer)] : []).concat(nodesToReplace.map(function (node, index) {
// Replace first node with token, delete remaining nodes. Guaranteed to be fontSize
if (index === 0) {
return fixer.replaceText(node, "".concat(getTokenProperty('font', matchingToken.tokenName, undefined, true)));
}
// We don't replace fontWeight/fontFamily/fontStyle here in case it occurs before the font property.
// Instead delete the original property and add below
return ObjectEntry.deleteEntry(node, context, fixer);
}),
// Make sure font weight/family/style properties are added AFTER font property to ensure they override corectly
fontWeightReplacement ? [fixer.insertTextAfter(fontSizeNode, ",\n".concat(fontWeightReplacement))] : [], fontFamilyReplacement ? [fixer.insertTextAfter(fontSizeNode, ",\n".concat(fontFamilyReplacement))] : [], fontStyleReplacement ? [fixer.insertTextAfter(fontSizeNode, ",\n".concat(fontStyleReplacement))] : []);
};
}
};