UNPKG

@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
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) })};`; }