@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
JavaScript
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;