UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

271 lines (253 loc) 13.5 kB
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))] : []); }; } };