UNPKG

@atlaskit/eslint-plugin-ui-styling-standard

Version:

The eslint plugin to enforce and educate on Atlassian's UI Styling Standard

271 lines (270 loc) • 14.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _contextCompat = require("@atlaskit/eslint-utils/context-compat"); var _schema = require("@atlaskit/eslint-utils/schema"); var _createRuleWithTypedConfig = require("../utils/create-rule-with-typed-config"); var _isSimpleSelector = require("../utils/is-simple-selector"); var _parseSelector = require("../utils/parse-selector"); var _styleCalls = require("../utils/style-calls"); var _walkStyleCallProperties = require("../utils/walk-style-call-properties"); var _constants = require("./constants"); var _lintSelector = require("./lint-selector"); var _walkCssMap = require("./walk-css-map"); function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } var ignoredAtRules = new Set(['@container', // ignored because it's covered by `no-container-queries` '@media', // ignored because it's covered by `no-nested-styles` '@supports', '@property', '@starting-style']); var canonicalMediaQueries = new Map([['media-above-xxs', '@media all'], ['media-above-xs', '@media (min-width: 30rem)'], ['media-above-sm', '@media (min-width: 48rem)'], ['media-above-md', '@media (min-width: 64rem)'], ['media-above-lg', '@media (min-width: 90rem)'], ['media-above-xl', '@media (min-width: 110.5rem)'], ['media-below-xs', '@media not all and (min-width: 30rem)'], ['media-below-sm', '@media not all and (min-width: 48rem)'], ['media-below-md', '@media not all and (min-width: 64rem)'], ['media-below-lg', '@media not all and (min-width: 90rem)'], ['media-below-xl', '@media not all and (min-width: 110.5rem)'], ['media-only-xxs', '@media (min-width: 0rem) and (max-width: 29.99rem)'], ['media-only-xs', '@media (min-width: 30rem) and (max-width: 47.99rem)'], ['media-only-sm', '@media (min-width: 48rem) and (max-width: 63.99rem)'], ['media-only-md', '@media (min-width: 64rem) and (max-width: 89.99rem)'], ['media-only-lg', '@media (min-width: 90rem) and (max-width: 110.49rem)'], ['media-only-xl', '@media (min-width: 110.5rem)'], ['media-dark-mode', '@media (prefers-color-scheme: dark)'], ['media-light-mode', '@media (prefers-color-scheme: light)'], ['media-reduced-motion', '@media (prefers-reduced-motion: reduce)'], ['media-reduced-transparency', '@media (prefers-reduced-transparency: reduce)'], ['media-forced-colors-active', '@media screen and (forced-colors: active)'], ['media-legacy-high-contrast', '@media screen and (-ms-high-contrast: active)'], ['media-forced-colors-or-legacy-high-contrast', '@media screen and (forced-colors: active), screen and (-ms-high-contrast: active)']]); var unwrapTypeScriptExpression = function unwrapTypeScriptExpression(node) { var expression = node; while (expression.type === 'TSAsExpression' || expression.type === 'TSTypeAssertion' || expression.type === 'TSNonNullExpression' || expression.type === 'TSSatisfiesExpression') { expression = expression.expression; } return expression; }; var isCanonicalTypedMediaKey = function isCanonicalTypedMediaKey(node, importedMediaTypes) { if (node.type !== 'TSSatisfiesExpression') { return false; } var expression = unwrapTypeScriptExpression(node); if (expression.type !== 'Literal' || typeof expression.value !== 'string') { return false; } var satisfiesExpression = node; while (((_satisfiesExpression = satisfiesExpression) === null || _satisfiesExpression === void 0 ? void 0 : _satisfiesExpression.type) === 'TSSatisfiesExpression') { var _satisfiesExpression, _satisfiesExpression$, _satisfiesExpression$2; var typeName = (_satisfiesExpression$ = satisfiesExpression.typeAnnotation) === null || _satisfiesExpression$ === void 0 || (_satisfiesExpression$ = _satisfiesExpression$.typeName) === null || _satisfiesExpression$ === void 0 ? void 0 : _satisfiesExpression$.name; if (((_satisfiesExpression$2 = satisfiesExpression.typeAnnotation) === null || _satisfiesExpression$2 === void 0 ? void 0 : _satisfiesExpression$2.type) === 'TSTypeReference' && typeName) { var source = importedMediaTypes.get(typeName); if (source && canonicalMediaQueries.get(source) === expression.value) { return true; } } satisfiesExpression = satisfiesExpression.expression; } return false; }; var getImportedMediaTypeSources = function getImportedMediaTypeSources(context) { var importedMediaTypes = new Map(); var _iterator = _createForOfIteratorHelper((0, _contextCompat.getSourceCode)(context).ast.body), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var statement = _step.value; if (statement.type !== 'ImportDeclaration' || typeof statement.source.value !== 'string') { continue; } var match = statement.source.value.match(/^@atlaskit\/css\/at-rules\/(media-[a-z-]+)$/); if (match) { var _iterator2 = _createForOfIteratorHelper(statement.specifiers), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var specifier = _step2.value; if (specifier.type === 'ImportDefaultSpecifier') { importedMediaTypes.set(specifier.local.name, match[1]); } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return importedMediaTypes; }; var rule = (0, _createRuleWithTypedConfig.createLintRuleWithTypedConfig)({ meta: { name: 'no-unsafe-selectors', docs: { description: 'Disallows use of nested styles in `css` functions.', recommended: true, severity: 'error' }, fixable: 'code', hasSuggestions: true, messages: { /** * Selector messages */ 'no-ambiguous-pseudos': "Prefix the pseudo-selector with & when it targets this element (for example &:hover); apply descendant styles directly to the child instead.", 'no-increased-specificity': "Replace chained nesting selectors such as && with one & selector, then resolve competing styles through local composition.", 'no-restricted-pseudos': "The {{pseudo}} pseudo-selector is unsupported. Use an allowed local pseudo such as &:hover, or pass explicit state as a prop and select a local cssMap variant.", 'no-legacy-pseudo-element-syntax': "Use a double colon for pseudo-elements, for example &::before or &::after, instead of the legacy single-colon syntax.", 'no-unparsable-selectors': "The selector '{{selectorText}}' could not be parsed. Correct its CSS syntax; if it is valid, report the selector and reproduction to #help-ui-styling-standard.", 'no-selectors-object': "Move selectors out of the selectors object into supported local style keys; apply child styles directly to each child element.", /** * At-rule messages */ 'no-keyframes-at-rules': "Replace the @keyframes at-rule with keyframes() from @compiled/react and reference its result through animationName.", 'no-restricted-at-rules': "{{atRule}} is unsupported. Use a supported @media, @supports or @property rule; for runtime conditions select a local style variant in JavaScript.", 'no-grouped-at-rules': "Flatten grouped at-rules into complete keys, for example \"@media (min-width: 48rem)\" instead of an \"@media\" object containing conditions.", 'no-noncanonical-media-query': "Match the literal query to its imported @atlaskit/css/at-rules type, including the exact breakpoint, units and comparison operator.", /** * Suggestion messages */ 'insert-nesting-selector': "Insert & before this pseudo-selector to target the element itself, for example &:hover." }, schema: { type: 'object', properties: { importSources: _schema.importSources, shouldAlwaysInsertNestingSelectorForAmbiguousPseudos: { type: 'boolean', default: true } }, additionalProperties: false } }, create: function create(context, config) { return { Program: function Program() { var importedMediaTypes = getImportedMediaTypeSources(context); var _iterator3 = _createForOfIteratorHelper((0, _styleCalls.getStyleCalls)(context)), _step3; try { var _loop = function _loop() { var styleCall = _step3.value; if (!config.importSources.includes(styleCall.importSource)) { return 1; // continue } if (styleCall.styleFunction === 'cssMap') { (0, _walkCssMap.walkCssMapCall)(styleCall.node, function (property) { var type = property.type, node = property.node; if (type === 'grouped-at-rules') { context.report({ node: node.key, messageId: 'no-grouped-at-rules' }); } else if (type === 'selectors') { context.report({ node: node.key, messageId: 'no-selectors-object' }); } }); } (0, _walkStyleCallProperties.walkStyleCallProperties)(styleCall, function (_ref) { var key = _ref.key, value = _ref.value; var keyType = key.type; if (value.type !== 'ObjectExpression' || keyType !== 'Literal' && keyType !== 'TSSatisfiesExpression') { return; } if (keyType === 'TSSatisfiesExpression') { var unwrappedKey = unwrapTypeScriptExpression(key); if (unwrappedKey.type === 'Literal' && typeof unwrappedKey.value === 'string') { if (unwrappedKey.value.startsWith('@media') && !isCanonicalTypedMediaKey(key, importedMediaTypes)) { context.report({ node: key, messageId: 'no-noncanonical-media-query' }); } return; } return; } var selectorText = key.value; if (typeof selectorText !== 'string') { return; } if (selectorText.includes('@')) { lintAtRule({ context: context, sourceNode: key, atRule: selectorText }); return; } if ((0, _isSimpleSelector.isSimpleSelector)(selectorText, { allowedPseudos: _constants.allowedPseudos, allowLeadingPseudo: styleCall.styleFunction === 'xcss' })) { return; } try { var selectorList = (0, _parseSelector.parseSelector)(context, selectorText); var _iterator4 = _createForOfIteratorHelper(selectorList.nodes), _step4; try { for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { var selector = _step4.value; (0, _lintSelector.lintSelector)({ context: context, sourceNode: key, selector: selector, config: config, isXcssCall: styleCall.styleFunction === 'xcss' }); } } catch (err) { _iterator4.e(err); } finally { _iterator4.f(); } } catch (_unused) { context.report({ node: key, messageId: 'no-unparsable-selectors', data: { selectorText: selectorText } }); } }); }; for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { if (_loop()) continue; } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } } }; } }); var _default = exports.default = rule; function lintAtRule(_ref2) { var context = _ref2.context, sourceNode = _ref2.sourceNode, atRule = _ref2.atRule; var matches = atRule.trim().match(/^@[A-z-]+/); if (!matches) { return; } var ruleName = matches[0]; if (ignoredAtRules.has(ruleName)) { return; } if (ruleName === '@keyframes') { context.report({ node: sourceNode, messageId: 'no-keyframes-at-rules' }); } else { context.report({ node: sourceNode, messageId: 'no-restricted-at-rules', data: { atRule: ruleName } }); } }