UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

148 lines (140 loc) 5.8 kB
import { isNodeOfType } from 'eslint-codemod-utils'; import { getScope } from '@atlaskit/eslint-utils/context-compat'; import { createLintRule } from '../utils/create-lint-rule'; import { findIdentifierInParentScope } from '../utils/find-in-parent'; var allowedPrefix = [':', '&:']; var allowedResponsiveImports = ['@atlaskit/primitives/responsive', '@atlaskit/primitives']; var unwrapSatisfiesExpression = function unwrapSatisfiesExpression(node) { while (node.type === 'TSSatisfiesExpression') { node = node.expression; } return node; }; /** * Tests against properties using the Design System primitives media object: [media.above.md] * @returns true if the property is a media query */ var isMediaObject = function isMediaObject(node, context) { if (node.type === 'MemberExpression') { if (node.object.type === 'MemberExpression' && node.object.object.type === 'Identifier') { var scope = getScope(context, node); var variable = findIdentifierInParentScope({ scope: scope, identifierName: node.object.object.name }); if (!variable) { return false; } var definition = variable.defs[0]; // Make sure it's coming from the primitives packages and isn't a bootleg media query or worse: nested styles if (isNodeOfType(definition.node, 'ImportSpecifier') && definition.node.parent && isNodeOfType(definition.node.parent, 'ImportDeclaration') && allowedResponsiveImports.includes(definition.node.parent.source.value)) { // This should match the name of the media object exported from packages/design-system/primitives/src/responsive/media-helper.tsx return definition.node.imported.type === 'Identifier' && definition.node.imported.name === 'media'; } } } return false; }; var parseSelector = function parseSelector(rawSelector) { if (typeof rawSelector !== 'string') { throw new Error('expected string'); } var selectors = rawSelector.split(',').map(function (selector) { return selector.trim(); }); return selectors; }; var getKeyValue = function getKeyValue(node, context) { node = unwrapSatisfiesExpression(node); if (node.type === 'Identifier') { return node.name; } if (node.type === 'Literal' && typeof node.value === 'string') { return node.value; } if (isMediaObject(node, context)) { return '@media'; } return ''; }; // const isWidthMediaQuery = (rawSelector: string): boolean => { // const selectors = parseSelector(rawSelector); // if (selectors[0].startsWith('@')) { // // If the selector includes a min-width/max-width query, return false - the primitives media object should be used instead: // // https://staging.atlassian.design/components/primitives/responsive/breakpoints/examples // // Otherwise return true, non-width queries are acceptable // return selectors.some( // (selector) => selector.includes('min-width') || selector.includes('max-width'), // ); // } // return false; // }; var isAllowedNestedSelector = function isAllowedNestedSelector(rawSelector) { if (rawSelector.trim() === '&') { // This can be written without the nest. return false; } var selectors = parseSelector(rawSelector); if (selectors[0].startsWith('@')) { // Bail early as it's an at selector. Width queries are handled by `isWidthMediaQuery`. return true; } return selectors.every(function (selector) { return selector === '&' || allowedPrefix.find(function (prefix) { return selector.startsWith(prefix); }); }); }; var isUsingDirectDataAttribute = function isUsingDirectDataAttribute(rawSelector) { var selectors = parseSelector(rawSelector); return selectors.some(function (selector) { return selector.startsWith('&['); }); }; var rule = createLintRule({ meta: { name: 'no-nested-styles', docs: { description: 'Disallows use of nested styles in `css` functions.', recommended: true, severity: 'error' }, messages: { // noWidthQueries: // 'Media queries that target min-width or max-width are not allowed. Use the media object provided by the Atlassian Design System instead. https://staging.atlassian.design/components/primitives/responsive/breakpoints/examples', noNestedStyles: 'Nested styles are not allowed as they can change unexpectedly when child markup changes and result in duplicates when extracting to CSS.', noDirectNestedStyles: "Styles applied with data attributes are not allowed, split them into discrete CSS declarations and apply them conditionally with JavaScript.\n\n```\nconst disabledStyles = css({ opacity: 0.5 });\n\n<div css={isDisabled && disabledStyles} />\n```\n" } }, create: function create(context) { return { 'CallExpression[callee.name=css] > ObjectExpression Property,CallExpression[callee.name=xcss] > ObjectExpression Property': function CallExpressionCalleeNameCss__ObjectExpression_PropertyCallExpressionCalleeNameXcss__ObjectExpression_Property(node) { if (node.type !== 'Property' || node.value.type !== 'ObjectExpression') { return; } if (isUsingDirectDataAttribute(getKeyValue(node.key, context))) { context.report({ node: node, messageId: 'noDirectNestedStyles' }); return; } // if (isWidthMediaQuery(getKeyValue(node.key as Rule.Node, context))) { // context.report({ // node, // messageId: 'noWidthQueries', // }); // return; // } if (!isAllowedNestedSelector(getKeyValue(node.key, context))) { context.report({ node: node, messageId: 'noNestedStyles' }); return; } } }; } }); export default rule;