@atlaskit/eslint-plugin-ui-styling-standard
Version:
The eslint plugin to enforce and educate on Atlassian's UI Styling Standard
194 lines (192 loc) • 6.51 kB
JavaScript
import { identifier, importDeclaration, importSpecifier, literal } from 'eslint-codemod-utils';
import { getAllComments } from '@atlaskit/eslint-utils/context-compat';
import { findVariable } from '@atlaskit/eslint-utils/find-variable';
import { createLintRule } from '../utils/create-rule';
import { isImportDefaultSpecifier } from './is-import-default-specifier';
import { isImportNamespaceSpecifier } from './is-import-namespace-specifier';
import { isImportSpecifier } from './is-import-specifier';
import { isSafeStyled } from './is-safe-styled';
import { isSafeUsage } from './is-safe-usage';
import { getFirstImportFromSource } from './utils';
const SUPPORTED_EMOTION_IMPORTS = new Set(['css', 'keyframes', 'ClassNames', 'jsx']);
const SUPPORTED_IMPORT_MAP = {
'@emotion/core': SUPPORTED_EMOTION_IMPORTS,
'@emotion/react': SUPPORTED_EMOTION_IMPORTS,
'@emotion/styled': new Set(['default']),
'styled-components': new Set(['default', 'css', 'keyframes'])
};
function isSupportedImportSource(importSource) {
return importSource in SUPPORTED_IMPORT_MAP;
}
const schema = {
type: 'array',
items: [{
type: 'object',
properties: {
canAutoFix: {
type: 'boolean'
}
},
additionalProperties: false
}]
};
const defaultConfig = {
canAutoFix: false
};
function readConfig(context) {
const [config] = context.options;
return Object.assign({}, defaultConfig, config);
}
const rule = createLintRule({
meta: {
name: 'use-compiled',
type: 'problem',
fixable: 'code',
docs: {
description: 'Ensures usage of `@compiled/react` or `@atlaskit/css` instead of other CSS-in-JS libraries',
recommended: true,
severity: 'error'
},
messages: {
'use-compiled': "Migrate '{{importSource}}' styling to @atlaskit/css or @compiled/react, updating imports and usages together. Verify the component rather than mixing styling engines on it."
},
schema
},
create(context) {
const config = readConfig(context);
return {
Program() {
const pragma = getAllComments(context).find(n => n.value.includes('@jsxImportSource @emotion/react'));
if (!pragma) {
return;
}
context.report({
messageId: 'use-compiled',
data: {
importSource: '@emotion/react'
},
loc: pragma.loc,
*fix(fixer) {
var _context$sourceCode;
if (!config.canAutoFix) {
return;
}
// @ts-ignore - Jira's ESLint v10 types expose sourceCode, platform still checks with ESLint v9.
const importDeclarations = ((_context$sourceCode = context.sourceCode) !== null && _context$sourceCode !== void 0 ? _context$sourceCode : context.getSourceCode()).ast.body.filter(node => node.type === 'ImportDeclaration');
const hasEmotionImport = importDeclarations.some(importDeclaration => {
const importSource = importDeclaration.source.value;
return importSource.startsWith('@emotion/');
});
const hasCompiledImport = importDeclarations.some(importDeclaration => importDeclaration.source.value === '@compiled/react');
if (hasEmotionImport || !hasCompiledImport) {
// Considering this unsafe to auto-fix
return;
}
yield fixer.replaceText(
// TODO: JFP-2823 - this type cast was added due to Jira's ESLint v9 migration
// @ts-ignore - types don't accept comment nodes
pragma, `/** @jsxImportSource @compiled/react */`);
}
});
},
ImportDeclaration(node) {
const importSource = node.source.value;
if (!isSupportedImportSource(importSource)) {
return;
}
context.report({
node: node.source,
messageId: 'use-compiled',
data: {
importSource
},
*fix(fixer) {
if (!config.canAutoFix) {
return;
}
const specifiers = getSpecifiers({
context,
node,
importSource
});
if (!specifiers) {
// Cannot safely auto-fix
return;
}
yield* fixImport({
context,
fixer,
node,
specifiers
});
}
});
}
};
}
});
export default rule;
function getSpecifiers({
context,
node,
importSource
}) {
const supportedImports = SUPPORTED_IMPORT_MAP[importSource];
// We aren't supporting namespace imports
if (node.specifiers.some(isImportNamespaceSpecifier)) {
return;
}
const defaultSpecifier = node.specifiers.find(isImportDefaultSpecifier);
if (defaultSpecifier) {
var _context$sourceCode2;
if (!supportedImports.has('default')) {
return;
}
const styled = findVariable({
identifier: defaultSpecifier.local,
// @ts-ignore - Jira's ESLint v10 types expose sourceCode, platform still checks with ESLint v9.
sourceCode: (_context$sourceCode2 = context.sourceCode) !== null && _context$sourceCode2 !== void 0 ? _context$sourceCode2 : context.getSourceCode()
});
if (!styled || !isSafeStyled(styled)) {
return;
}
}
const namedSpecifiers = node.specifiers.filter(isImportSpecifier);
if (!namedSpecifiers.every(specifier => isSafeUsage({
context,
specifier,
supportedImports
}))) {
return;
}
if (defaultSpecifier) {
return [...namedSpecifiers,
// ASSUMPTION: the default import, if allowed, is for `styled`
importSpecifier({
imported: identifier('styled'),
local: node.specifiers[0].local
})];
}
return namedSpecifiers;
}
function* fixImport({
context,
fixer,
node,
specifiers
}) {
const compiledImport = getFirstImportFromSource(context, '@compiled/react');
if (compiledImport) {
yield fixer.remove(node);
yield fixer.replaceText(compiledImport, buildImportDeclaration(compiledImport.specifiers.concat(specifiers), '@compiled/react'));
} else {
yield fixer.replaceText(node, buildImportDeclaration(specifiers, '@compiled/react'));
}
}
function buildImportDeclaration(specifiers, source) {
// @ts-ignore - this needs to be `ts-ignore` because this only errors in jira's typechecking due to update, not platform's
return `${importDeclaration({
specifiers,
source: literal(source)
})};`;
}