@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
148 lines (140 loc) • 5.8 kB
JavaScript
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;