UNPKG

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

Version:

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

167 lines • 8.21 kB
import estraverse from 'estraverse'; import { getScope, getSourceCode } from '@atlaskit/eslint-utils/context-compat'; import { getImportSources, isCss, isCssMap, isKeyframes, isStyled, isXcss } from '@atlaskit/eslint-utils/is-supported-import'; import { createLintRule } from '../utils/create-rule'; const schema = [{ type: 'object', properties: { importSources: { type: 'array', items: { type: 'string' }, uniqueItems: true } } }]; const unwrapTypeScriptExpression = node => { let expression = node; while (expression.type === 'TSAsExpression' || expression.type === 'TSTypeAssertion' || expression.type === 'TSNonNullExpression' || expression.type === 'TSSatisfiesExpression') { expression = expression.expression; } return expression; }; const isStaticContainerKey = node => { var _expression$expressio; const expression = unwrapTypeScriptExpression(node); if (expression.type === 'Literal') { return typeof expression.value === 'string'; } return expression.type === 'TemplateLiteral' && ((_expression$expressio = expression.expressions) === null || _expression$expressio === void 0 ? void 0 : _expression$expressio.length) === 0; }; const getStaticString = node => { var _expression$expressio2; if (!node) { return undefined; } const expression = unwrapTypeScriptExpression(node); if (expression.type === 'Literal') { const value = expression.value; return typeof value === 'string' ? value : undefined; } if (expression.type === 'TemplateLiteral' && ((_expression$expressio2 = expression.expressions) === null || _expression$expressio2 === void 0 ? void 0 : _expression$expressio2.length) === 0) { var _expression$quasis, _expression$quasis$, _expression$quasis$$v; return (_expression$quasis = expression.quasis) === null || _expression$quasis === void 0 ? void 0 : (_expression$quasis$ = _expression$quasis[0]) === null || _expression$quasis$ === void 0 ? void 0 : (_expression$quasis$$v = _expression$quasis$.value) === null || _expression$quasis$$v === void 0 ? void 0 : _expression$quasis$$v.raw; } return undefined; }; const getPropertyName = node => { const expression = unwrapTypeScriptExpression(node); return expression.type === 'Identifier' ? expression.name : getStaticString(expression); }; const isDimensionQuery = query => { // Policy source: https://hello.atlassian.net/wiki/spaces/~7012119e15ecf78c74278a34bd6681745f7bb/pages/2595609668/What+would+it+take+to+get+container+queries+working const condition = query.replace(/^@container\s+[-_a-zA-Z][-_a-zA-Z0-9]*\s*/, ''); return !condition.includes('style(') && /\b(?:width|height|min-width|max-width|min-height|max-height)\b/i.test(condition); }; const getContainerName = query => { const match = query.match(/^@container\s+([-_a-zA-Z][-_a-zA-Z0-9]*)\b/); return match === null || match === void 0 ? void 0 : match[1]; }; const isContainerDeclaration = node => { return getPropertyName(node) === 'containerName'; }; const isImportedContainerType = (node, context) => { let expression = node; while (expression.type === 'TSSatisfiesExpression') { var _typeAnnotation$typeN; const { typeAnnotation } = expression; if ((typeAnnotation === null || typeAnnotation === void 0 ? void 0 : typeAnnotation.type) === 'TSTypeReference' && ((_typeAnnotation$typeN = typeAnnotation.typeName) === null || _typeAnnotation$typeN === void 0 ? void 0 : _typeAnnotation$typeN.type) === 'Identifier') { const typeName = typeAnnotation.typeName.name; const isImported = getSourceCode(context).ast.body.some(statement => statement.type === 'ImportDeclaration' && statement.source.value === '@atlaskit/css/at-rules/container' && statement.specifiers.some(specifier => specifier.type === 'ImportDefaultSpecifier' && specifier.local.name === typeName)); if (isImported) { return true; } } expression = expression.expression; } return false; }; const isContainerQuery = node => { var _expression$quasis2; const expression = unwrapTypeScriptExpression(node); if (expression.type === 'Literal' && typeof expression.value === 'string') { return expression.value.includes('@container'); } return expression.type === 'TemplateLiteral' && Boolean((_expression$quasis2 = expression.quasis) === null || _expression$quasis2 === void 0 ? void 0 : _expression$quasis2.some(quasi => { var _quasi$value, _quasi$value$raw; return (_quasi$value = quasi.value) === null || _quasi$value === void 0 ? void 0 : (_quasi$value$raw = _quasi$value.raw) === null || _quasi$value$raw === void 0 ? void 0 : _quasi$value$raw.includes('@container'); })); }; const rule = createLintRule({ meta: { name: 'no-container-queries', docs: { description: 'Prevents usage of @container query within css styling', recommended: true, severity: 'error' }, messages: { 'no-container-queries': "Use a named container declared in this file and a width/height condition; type the static query key with satisfies and the default Container import from @atlaskit/css/at-rules/container.", 'no-container-style-queries': "Replace the style query with an explicit prop and local cssMap variant, or use a width/height-based query when size is the intended condition.", 'no-unbound-container-queries': "Name the container in the same file and reference it in the query, for example containerName: \"card\" with \"@container card (min-width: 30rem)\".", 'no-mismatched-container-name': "Use the same container name in this @container query and the containerName declaration in this file; rename the mismatched declaration or query.", 'no-non-dimension-container-queries': "Use a width/height condition for this named local container. Represent other conditions as explicit props selecting local styles." }, type: 'problem', schema }, create(context) { const importSources = getImportSources(context); const containerNames = new Set(); return { Program(node) { estraverse.traverse(node, { enter(child) { if (child.type !== 'Property') { return; } const property = child; if (isContainerDeclaration(property.key)) { const id = getStaticString(property.value); if (id) { containerNames.add(id); } } }, fallback: 'iteration' }); }, CallExpression(node) { const { references } = getScope(context, node); if (isCss(node.callee, references, importSources) || isStyled(node.callee, references, importSources) || isKeyframes(node.callee, references, importSources) || isCssMap(node.callee, references, importSources) || isXcss(node.callee, references, importSources)) { estraverse.traverse(node, { enter(node) { if (node.type !== 'Property') { return; } const key = node.key; if (!isContainerQuery(key)) { return; } const query = getStaticString(key); const queryName = query ? getContainerName(query) : undefined; const reportMessageId = !query ? 'no-container-queries' : query.includes('style(') ? 'no-container-style-queries' : !queryName ? 'no-unbound-container-queries' : !containerNames.has(queryName) ? 'no-mismatched-container-name' : !isDimensionQuery(query) ? 'no-non-dimension-container-queries' : 'no-container-queries'; if (isImportedContainerType(key, context) && isStaticContainerKey(key.expression) && query && queryName && containerNames.has(queryName) && isDimensionQuery(query)) { return; } context.report({ node: node.key, messageId: reportMessageId }); }, /** * This is needed to handle unknown node types. Otherwise an error is thrown. */ fallback: 'iteration' }); } } }; } }); export default rule;