chore: update eslint-plugin-eui to 0.1.1 (#210082)

## Summary

Bring in the changes from https://github.com/elastic/eui/pull/8304,
specifically ESLint rules:

- `no-restricted-eui-imports`
- `no-css-color` (migrated from `@kbn/eslint-plugin-css`)
- `prefer-css-attribute-for-eui-components` (migrated from
`@kbn/eslint-plugin-css`)

Relates to https://github.com/elastic/eui/issues/8201,
https://github.com/elastic/eui-private/issues/275

## QA

### Instructions

1. Checkout this branch: `gh pr checkout 210082`.
2. Reinstall dependencies: `yarn kbn bootstrap`.
3. See output of ESLint. There should be no errors.
4. Test below cases.

### Test cases

#### `no-restricted-eui-imports`

Example files:

- JSON imports: `src/platform/packages/shared/kbn-ui-theme/src/theme.ts`
- `@kbn/ui-theme`:
`src/platform/plugins/private/vis_types/vega/public/data_model/utils.ts`

#### `no-css-color`

Example file:
`src/platform/plugins/shared/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx:50`

![Screenshot 2025-02-26 at 15 01
53](https://github.com/user-attachments/assets/ec6f49bd-5832-4d1c-9cfd-74c40ad5498e)

#### `prefer-css-attribute-for-eui-components`

Example file:
`x-pack/examples/alerting_example/public/alert_types/always_firing.tsx:166`
This commit is contained in:
Weronika Olejniczak 2025-04-02 14:06:17 +02:00 committed by GitHub
parent ecd83ce211
commit 7e46d2e756
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 28 additions and 1085 deletions

View file

@ -280,7 +280,7 @@ const RESTRICTED_IMPORTS = [
module.exports = {
root: true,
extends: ['plugin:@elastic/eui/recommended', '@kbn/eslint-config'],
extends: ['@kbn/eslint-config'],
overrides: [
/**

1
.github/CODEOWNERS vendored
View file

@ -55,7 +55,6 @@ packages/kbn-dependency-ownership @elastic/kibana-security
packages/kbn-dependency-usage @elastic/kibana-security
packages/kbn-docs-utils @elastic/kibana-operations
packages/kbn-eslint-config @elastic/kibana-operations
packages/kbn-eslint-plugin-css @elastic/appex-sharedux
packages/kbn-eslint-plugin-disable @elastic/kibana-operations
packages/kbn-eslint-plugin-eslint @elastic/kibana-operations
packages/kbn-eslint-plugin-eui-a11y @elastic/obs-ux-infra_services-team

View file

@ -1372,7 +1372,7 @@
"@cypress/debugging-proxy": "2.0.1",
"@cypress/grep": "^4.0.1",
"@cypress/webpack-preprocessor": "^6.0.2",
"@elastic/eslint-plugin-eui": "0.0.2",
"@elastic/eslint-plugin-eui": "0.1.1",
"@elastic/makelogs": "^6.1.1",
"@elastic/synthetics": "^1.18.0",
"@emotion/babel-preset-css-prop": "^11.11.0",
@ -1475,7 +1475,6 @@
"@kbn/es": "link:src/platform/packages/shared/kbn-es",
"@kbn/es-archiver": "link:src/platform/packages/shared/kbn-es-archiver",
"@kbn/eslint-config": "link:packages/kbn-eslint-config",
"@kbn/eslint-plugin-css": "link:packages/kbn-eslint-plugin-css",
"@kbn/eslint-plugin-disable": "link:packages/kbn-eslint-plugin-disable",
"@kbn/eslint-plugin-eslint": "link:packages/kbn-eslint-plugin-eslint",
"@kbn/eslint-plugin-eui-a11y": "link:packages/kbn-eslint-plugin-eui-a11y",

View file

@ -20,7 +20,13 @@
const { USES_STYLED_COMPONENTS } = require('@kbn/babel-preset/styled_components_files');
module.exports = {
extends: ['./javascript.js', './typescript.js', './jest.js', './react.js'],
extends: [
'./javascript.js',
'./typescript.js',
'./jest.js',
'./react.js',
'plugin:@elastic/eui/recommended',
],
plugins: [
'@kbn/eslint-plugin-disable',
@ -29,7 +35,7 @@ module.exports = {
'@kbn/eslint-plugin-telemetry',
'@kbn/eslint-plugin-i18n',
'@kbn/eslint-plugin-eui-a11y',
'@kbn/eslint-plugin-css',
'@elastic/eui',
'eslint-plugin-depend',
'prettier',
],
@ -129,16 +135,6 @@ module.exports = {
exclude: USES_STYLED_COMPONENTS,
disallowedMessage: `Prefer using @emotion/react instead. To use styled-components, ensure you plugin is enabled in packages/kbn-babel-preset/styled_components_files.js.`,
},
...[
'@elastic/eui/dist/eui_theme_amsterdam_light.json',
'@elastic/eui/dist/eui_theme_amsterdam_dark.json',
'@elastic/eui/dist/eui_theme_borealis_light.json',
'@elastic/eui/dist/eui_theme_borealis_dark.json',
].map((from) => ({
from,
to: false,
disallowedMessage: `Use "@kbn/ui-theme" to access theme vars.`,
})),
{
from: '@kbn/test/jest',
to: '@kbn/test-jest-helpers',
@ -335,9 +331,20 @@ module.exports = {
'@kbn/imports/no_boundary_crossing': 'error',
'@kbn/imports/no_group_crossing_manifests': 'error',
'@kbn/imports/no_group_crossing_imports': 'error',
'@kbn/css/no_css_color': 'warn',
'no-new-func': 'error',
'no-implied-eval': 'error',
'no-prototype-builtins': 'error',
/**
* EUI Team rules
*/
'@elastic/eui/no-restricted-eui-imports': [
'warn',
{
patterns: ['@kbn/ui-theme'],
message: 'For client-side, please use `useEuiTheme` instead.',
},
],
},
};

View file

@ -1,129 +0,0 @@
---
id: kibSharedUXEslintPluginCSS
slug: /kibana-dev-docs/shared-ux/packages/kbn-eslint-plugin-css
title: '@kbn/eslint-plugin-design-tokens'
description: Custom ESLint rules to guardrails for using eui in the Kibana repository
date: 2024-11-19
tags: ['kibana', 'dev', 'contributor', 'shared_ux', 'eslint', 'eui']
---
# Summary
`@kbn/eslint-plugin-css` is an ESLint plugin providing custom ESLint rules to help setup guardrails for using eui in the Kibana repo especially around styling.
The aim of this package is to help engineers to modify EUI components in a much complaint way.
If a rule does not behave as you expect or you have an idea of how these rules can be improved, please reach out to the Shared UX team.
# Rules
## `@kbn/css/no_css_color`
This rule warns engineers to not use literal css color in the codebase, particularly for CSS properties that apply color to
either the html element or text nodes, but rather urge users to defer to using the color tokens provided by EUI.
This rule kicks in on the following JSXAttributes; `style`, `className` and `css` and supports various approaches to providing styling declarations.
### Example
The following code:
```
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
import React from 'react';
import { EuiText } from '@elastic/eui';
function MyComponent() {
return (
<EuiText style={{ color: 'red' }}>You know, for search</EuiText>
)
}
```
```
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
import React from 'react';
import { EuiText } from '@elastic/eui';
function MyComponent() {
const style = {
color: 'red'
}
return (
<EuiText style={{ color: style.color }}>You know, for search</EuiText>
)
}
```
```
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
import React from 'react';
import { EuiText } from '@elastic/eui';
function MyComponent() {
const colorValue = '#dd4040';
return (
<EuiText style={{ color: colorValue }}>You know, for search</EuiText>
)
}
```
will all raise an eslint report with an appropriate message of severity that matches the configuration of the rule, further more all the examples above
will also match for when the attribute in question is `css`. The `css` attribute will also raise a report the following cases below;
```
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
import React from 'react';
import { css } from '@emotion/css';
import { EuiText } from '@elastic/eui';
function MyComponent() {
return (
<EuiText css={css`color: '#dd4040' `}>You know, for search</EuiText>
)
}
```
```
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
import React from 'react';
import { EuiText } from '@elastic/eui';
function MyComponent() {
return (
<EuiText css={() => ({ color: '#dd4040' })}>You know, for search</EuiText>
)
}
```
A special case is also covered for the `className` attribute, where the rule will also raise a report for the following case below;
```
// Filename: /x-pack/plugins/observability_solution/observability/public/my_component.tsx
import React from 'react';
import { css } from '@emotion/css';
import { EuiText } from '@elastic/eui';
function MyComponent() {
return (
<EuiText className={css`color: '#dd4040'`}>You know, for search</EuiText>
)
}
```
it's worth pointing out that although the examples provided are specific to EUI components, this rule applies to all JSX elements.
## `@kbn/css/prefer_css_attributes_for_eui_components`
This rule warns engineers to use the `css` attribute for EUI components instead of the `style` attribute.

View file

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { NoCssColor } from './src/rules/no_css_color';
import { PreferCSSAttributeForEuiComponents } from './src/rules/prefer_css_attribute_for_eui_components';
/**
* Custom ESLint rules, included as `'@kbn/eslint-plugin-design-tokens'` in the kibana eslint config
* @internal
*/
export const rules = {
no_css_color: NoCssColor,
prefer_css_attributes_for_eui_components: PreferCSSAttributeForEuiComponents,
};

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-eslint-plugin-css'],
};

View file

@ -1,6 +0,0 @@
{
"type": "shared-common",
"id": "@kbn/eslint-plugin-css",
"devOnly": true,
"owner": "@elastic/appex-sharedux"
}

View file

@ -1,6 +0,0 @@
{
"name": "@kbn/eslint-plugin-css",
"version": "1.0.0",
"private": true,
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}

View file

@ -1,266 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { RuleTester } from 'eslint';
import { NoCssColor } from './no_css_color';
const tsTester = [
'@typescript-eslint/parser',
new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
ecmaFeatures: {
jsx: true,
},
},
}),
] as const;
const babelTester = [
'@babel/eslint-parser',
new RuleTester({
parser: require.resolve('@babel/eslint-parser'),
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
requireConfigFile: false,
babelOptions: {
presets: ['@kbn/babel-preset/node_preset'],
},
},
}),
] as const;
const invalid: RuleTester.InvalidTestCase[] = [
{
name: 'Raises an error when a CSS color is used in a JSX style attribute',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
function TestComponent() {
return (
<EuiCode style={{ color: '#dd4040' }}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCssColorSpecific' }],
},
{
name: 'Raises an error when a CSS color references a string variable that is passed to style prop of a JSX element',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
function TestComponent() {
const codeColor = '#dd4040';
return (
<EuiCode style={{ color: codeColor }}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCSSColorSpecificDeclaredVariable' }],
},
{
name: 'Raises an error when a CSS color is used in an object variable that is passed to style prop of a JSX element',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
function TestComponent() {
const codeStyle = { color: '#dd4040' };
return (
<EuiCode style={codeStyle}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCSSColorSpecificDeclaredVariable' }],
},
{
name: 'Raises an error when an object property that is a literal CSS color is used for the background property in a JSX style attribute',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
function TestComponent() {
const baseStyle = { background: 'rgb(255, 255, 255)' };
return (
<EuiCode style={{ background: baseStyle.background }}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCSSColorSpecificDeclaredVariable' }],
},
{
name: 'Raises an error when a CSS color is used in a variable that is spread into another variable that is passed to style prop of a JSX element',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
function TestComponent() {
const baseStyle = { background: 'rgb(255, 255, 255)' };
const codeStyle = { margin: '5px', ...baseStyle };
return (
<EuiCode style={codeStyle}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCSSColorSpecificDeclaredVariable' }],
},
{
name: 'Raises an error when a CSS color is used for the background property in a JSX style attribute',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
function TestComponent() {
return (
<EuiCode style={{ background: '#dd4040' }}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCssColorSpecific' }],
},
{
name: 'Raises an error when a CSS color for the color property is used in a JSX css attribute for EuiComponents',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
function TestComponent() {
return (
<EuiCode css={{ color: '#dd4040' }}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCssColorSpecific' }],
},
{
name: 'Raises an error when a CSS color for the color property is used in with the tagged template css function',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import { css } from '@emotion/css';
const codeColor = css\` color: #dd4040; \`;
`,
errors: [{ messageId: 'noCssColor' }],
},
{
name: 'Raises an error when a CSS color for the color property is used in a JSX css attribute for EuiComponents with the css template function',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
import { css } from '@emotion/css';
function TestComponent() {
return (
<EuiCode css={css\` color: #dd4040; \`}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCssColor' }],
},
{
name: 'Raises an error when a CSS color for the color property is used in a JSX className attribute for EuiComponents with the css template function defined outside the scope of the component',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
import { css } from '@emotion/css';
const codeCss = css({
color: '#dd4040',
})
function TestComponent() {
return (
<EuiCode css={codeCss}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCSSColorSpecificDeclaredVariable' }],
},
{
name: 'Raises an error when a CSS color for the color property is used in a JSX className attribute for EuiComponents with the css template function defined outside the scope of the component',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
import { css } from '@emotion/css';
const codeCss = css\` color: #dd4040; \`
function TestComponent() {
return (
<EuiCode css={codeCss}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCssColor' }],
},
{
name: 'Raises an error when a CSS color for the color property is used in a JSX css attribute for EuiComponents with an arrow function',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
function TestComponent() {
return (
<EuiCode css={() => ({ color: '#dd4040' })}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCssColorSpecific' }],
},
{
name: 'Raises an error when a CSS color for the color property is used in a JSX css attribute for EuiComponents with a regular function',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
function TestComponent() {
return (
<EuiCode css={function () { return { color: '#dd4040' }; }}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCssColorSpecific' }],
},
{
name: 'Raises an error when a CSS color for the color property is used in a JSX className attribute for EuiComponents with the css template function',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
import { css } from '@emotion/css';
function TestComponent() {
return (
<EuiCode className={css\` color: #dd4040; \`}>This is a test</EuiCode>
)
}`,
errors: [{ messageId: 'noCssColor' }],
},
];
const valid: RuleTester.ValidTestCase[] = [
{
name: 'Does not raise an error when a CSS color is not used in a JSX css prop attribute',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
import { EuiCode } from '@elastic/eui';
import { css } from '@emotion/react';
function TestComponent() {
return (
<EuiCode css={css\`
border-top: none;
border-radius: 0 0 6px 6px;
\`}>This is a test</EuiCode>
)
}`,
},
];
for (const [name, tester] of [tsTester, babelTester]) {
describe(name, () => {
tester.run('@kbn/no_css_color', NoCssColor, {
valid,
invalid,
});
});
}

View file

@ -1,453 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { Rule } from 'eslint';
import { CSSStyleDeclaration } from 'cssstyle';
import type { TSESTree } from '@typescript-eslint/typescript-estree';
/**
* @description List of superset css properties that can apply color to html box element elements and text nodes, leveraging the
* css style package allows us to directly singly check for these properties even if the actual declaration was written using the shorthand form
*/
const propertiesSupportingCssColor = ['color', 'background', 'border'];
/**
* @description Builds off the existing color definition to match css declarations that can apply color to
* html elements and text nodes for string declarations
*/
const htmlElementColorDeclarationRegex = RegExp(
String.raw`(${propertiesSupportingCssColor.join('|')})`
);
const checkPropertySpecifiesInvalidCSSColor = ([property, value]: string[]) => {
if (!property || !value) return false;
const style = new CSSStyleDeclaration();
// @ts-ignore the types for this packages specifies an index signature of number, alongside other valid CSS properties
style[property.trim()] = typeof value === 'string' ? value.trim() : value;
const anchor = propertiesSupportingCssColor.find((resolvedProperty) =>
property.includes(resolvedProperty)
);
if (!anchor) return false;
// build the resolved color property to check if the value is a string after parsing the style declaration
const resolvedColorProperty = anchor === 'color' ? 'color' : anchor + 'Color';
// in trying to keep this rule simple, it's enough if we get a value back, because if it was an identifier we would have been able to set a value within this invocation
// @ts-ignore the types for this packages specifics an index signature of number, alongside other valid CSS properties
return Boolean(style[resolvedColorProperty]);
};
const resolveMemberExpressionRoot = (node: TSESTree.MemberExpression): TSESTree.Identifier => {
if (node.object.type === 'MemberExpression') {
return resolveMemberExpressionRoot(node.object);
}
return node.object as TSESTree.Identifier;
};
/**
* @description method to inspect values of interest found on an object
*/
const raiseReportIfPropertyHasInvalidCssColor = (
context: Rule.RuleContext,
propertyNode: TSESTree.Property,
messageToReport: Rule.ReportDescriptor
) => {
let didReport = false;
if (
propertyNode.key.type === 'Identifier' &&
!htmlElementColorDeclarationRegex.test(propertyNode.key.name)
) {
return didReport;
}
if (propertyNode.value.type === 'Literal') {
if (
(didReport = checkPropertySpecifiesInvalidCSSColor([
// @ts-expect-error the key name is present in this scenario
propertyNode.key.name,
propertyNode.value.value,
]))
) {
context.report(messageToReport);
}
} else if (propertyNode.value.type === 'Identifier') {
const identifierDeclaration = context.sourceCode
// @ts-expect-error
.getScope(propertyNode)
.variables.find(
(variable) => variable.name === (propertyNode.value as TSESTree.Identifier).name!
);
if (
identifierDeclaration?.defs[0].node.init?.type === 'Literal' &&
checkPropertySpecifiesInvalidCSSColor([
// @ts-expect-error the key name is present in this scenario
propertyNode.key.name,
(identifierDeclaration.defs[0].node.init as TSESTree.Literal).value as string,
])
) {
context.report({
loc: propertyNode.value.loc,
messageId: 'noCSSColorSpecificDeclaredVariable',
data: {
// @ts-expect-error the key name is always present else this code will not execute
property: String(propertyNode.key.name),
line: String(propertyNode.value.loc.start.line),
variableName: propertyNode.value.name,
},
});
didReport = true;
}
} else if (propertyNode.value.type === 'MemberExpression') {
// @ts-expect-error we ignore the case where this node could be a private identifier
const MemberExpressionLeafName = propertyNode.value.property.name;
const memberExpressionRootName = resolveMemberExpressionRoot(propertyNode.value).name;
const expressionRootDeclaration = context.sourceCode
// @ts-expect-error
.getScope(propertyNode)
.variables.find((variable) => variable.name === memberExpressionRootName);
const expressionRootDeclarationInit = expressionRootDeclaration?.defs[0].node.init;
if (expressionRootDeclarationInit?.type === 'ObjectExpression') {
(expressionRootDeclarationInit as TSESTree.ObjectExpression).properties.forEach(
(property) => {
// This is a naive approach expecting the value to be at depth 1, we should actually be traversing the object to the same depth as the expression
if (
property.type === 'Property' &&
property.key.type === 'Identifier' &&
property.key?.name === MemberExpressionLeafName
) {
raiseReportIfPropertyHasInvalidCssColor(context, property, {
loc: propertyNode.value.loc,
messageId: 'noCSSColorSpecificDeclaredVariable',
data: {
// @ts-expect-error the key name is always present else this code will not execute
property: String(propertyNode.key.name),
line: String(propertyNode.value.loc.start.line),
variableName: memberExpressionRootName,
},
});
}
}
);
} else if (expressionRootDeclarationInit?.type === 'CallExpression') {
// TODO: if this object was returned from invoking a function the best we can do is probably validate that the method invoked is one that returns an euitheme object
}
}
return didReport;
};
/**
*
* @description style object declaration have a depth of 1, this function handles the properties of the object
*/
const handleObjectProperties = (
context: Rule.RuleContext,
propertyParentNode: TSESTree.JSXAttribute,
property: TSESTree.ObjectLiteralElement,
reportMessage: Rule.ReportDescriptor
) => {
if (property.type === 'Property') {
raiseReportIfPropertyHasInvalidCssColor(context, property, reportMessage);
} else if (property.type === 'SpreadElement') {
const spreadElementIdentifierName = (property.argument as TSESTree.Identifier).name;
const spreadElementDeclaration = context.sourceCode
// @ts-expect-error
.getScope(propertyParentNode!.value.expression!)
.references.find((ref) => ref.identifier.name === spreadElementIdentifierName)?.resolved;
if (!spreadElementDeclaration) {
return;
}
reportMessage = {
loc: propertyParentNode.loc,
messageId: 'noCSSColorSpecificDeclaredVariable',
data: {
// @ts-expect-error the key name is always present else this code will not execute
property: String(property.argument.name),
variableName: spreadElementIdentifierName,
line: String(property.loc.start.line),
},
};
const spreadElementDeclarationNode = spreadElementDeclaration.defs[0].node.init;
// evaluate only statically defined declarations, other possibilities like callExpressions in this context complicate things
if (spreadElementDeclarationNode?.type === 'ObjectExpression') {
(spreadElementDeclarationNode as TSESTree.ObjectExpression).properties.forEach(
(spreadProperty) => {
handleObjectProperties(context, propertyParentNode, spreadProperty, reportMessage);
}
);
}
}
};
export const NoCssColor: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
description: 'Use color definitions from eui theme as opposed to CSS color values',
category: 'Best Practices',
recommended: true,
url: 'https://eui.elastic.co/#/theming/colors/values',
},
messages: {
noCSSColorSpecificDeclaredVariable:
'Avoid using a literal CSS color value for "{{property}}", use an EUI theme color instead in declared variable {{variableName}} on line {{line}}',
noCssColorSpecific:
'Avoid using a literal CSS color value for "{{property}}", use an EUI theme color instead',
noCssColor: 'Avoid using a literal CSS color value, use an EUI theme color instead',
},
schema: [],
},
create(context) {
return {
// accounts for instances where declarations are created using the template tagged css function
TaggedTemplateExpression(node) {
if (
node.tag.type !== 'Identifier' ||
(node.tag.type === 'Identifier' && node.tag.name !== 'css')
) {
return;
}
for (let i = 0; i < node.quasi.quasis.length; i++) {
const declarationTemplateNode = node.quasi.quasis[i];
if (htmlElementColorDeclarationRegex.test(declarationTemplateNode.value.raw)) {
const cssText = declarationTemplateNode.value.raw.replace(/(\{|\}|\\n)/g, '').trim();
cssText.split(';').forEach((declaration) => {
if (
declaration.length > 0 &&
checkPropertySpecifiesInvalidCSSColor(declaration.split(':'))
) {
context.report({
node: declarationTemplateNode,
messageId: 'noCssColor',
});
}
});
}
}
},
JSXAttribute(node: TSESTree.JSXAttribute) {
if (!(node.name.name === 'style' || node.name.name === 'css')) {
return;
}
/**
* @description Accounts for instances where a variable is used to define a style object
*
* @example
* const codeStyle = { color: '#dd4040' };
* <EuiCode style={codeStyle}>This is an example</EuiCode>
*
* @example
* const codeStyle = { color: '#dd4040' };
* <EuiCode css={codeStyle}>This is an example</EuiCode>
*
* @example
* const codeStyle = css({ color: '#dd4040' });
* <EuiCode css={codeStyle}>This is an example</EuiCode>
*/
if (
node.value?.type === 'JSXExpressionContainer' &&
node.value.expression.type === 'Identifier'
) {
const styleVariableName = node.value.expression.name;
const nodeScope = context.sourceCode.getScope(node.value.expression);
const variableDeclarationMatches = nodeScope.references.find(
(ref) => ref.identifier.name === styleVariableName
)?.resolved;
let variableInitializationNode;
if ((variableInitializationNode = variableDeclarationMatches?.defs?.[0]?.node?.init)) {
if (variableInitializationNode.type === 'ObjectExpression') {
// @ts-ignore
variableInitializationNode.properties.forEach((property) => {
handleObjectProperties(context, node, property, {
loc: property.loc,
messageId: 'noCSSColorSpecificDeclaredVariable',
data: {
property:
property.type === 'SpreadElement'
? String(property.argument.name)
: String(property.key.name),
variableName: styleVariableName,
line: String(property.loc.start.line),
},
});
});
} else if (
variableInitializationNode.type === 'CallExpression' &&
variableInitializationNode.callee.name === 'css'
) {
const cssFunctionArgument = variableInitializationNode.arguments[0];
if (cssFunctionArgument.type === 'ObjectExpression') {
// @ts-ignore
cssFunctionArgument.properties.forEach((property) => {
handleObjectProperties(context, node, property, {
loc: node.loc,
messageId: 'noCSSColorSpecificDeclaredVariable',
data: {
property:
property.type === 'SpreadElement'
? String(property.argument.name)
: String(property.key.name),
variableName: styleVariableName,
line: String(property.loc.start.line),
},
});
});
}
}
}
return;
}
/**
*
* @description Accounts for instances where a style object is inlined in the JSX attribute
*
* @example
* <EuiCode style={{ color: '#dd4040' }}>This is an example</EuiCode>
*
* @example
* <EuiCode css={{ color: '#dd4040' }}>This is an example</EuiCode>
*
* @example
* const styleRules = { color: '#dd4040' };
* <EuiCode style={{ color: styleRules.color }}>This is an example</EuiCode>
*
* @example
* const styleRules = { color: '#dd4040' };
* <EuiCode css={{ color: styleRules.color }}>This is an example</EuiCode>
*/
if (
node.value?.type === 'JSXExpressionContainer' &&
node.value.expression.type === 'ObjectExpression'
) {
const declarationPropertiesNode = node.value.expression.properties;
declarationPropertiesNode?.forEach((property) => {
handleObjectProperties(context, node, property, {
loc: property.loc,
messageId: 'noCssColorSpecific',
data: {
property:
property.type === 'SpreadElement'
? // @ts-expect-error the key name is always present else this code will not execute
String(property.argument.name)
: // @ts-expect-error the key name is always present else this code will not execute
String(property.key.name),
},
});
});
return;
}
if (node.name.name === 'css' && node.value?.type === 'JSXExpressionContainer') {
/**
* @example
* <EuiCode css={`{ color: '#dd4040' }`}>This is an example</EuiCode>
*/
if (node.value.expression.type === 'TemplateLiteral') {
for (let i = 0; i < node.value.expression.quasis.length; i++) {
const declarationTemplateNode = node.value.expression.quasis[i];
if (htmlElementColorDeclarationRegex.test(declarationTemplateNode.value.raw)) {
const cssText = declarationTemplateNode.value.raw
.replace(/(\{|\}|\\n)/g, '')
.trim();
cssText.split(';').forEach((declaration) => {
if (
declaration.length > 0 &&
checkPropertySpecifiesInvalidCSSColor(declaration.split(':'))
) {
context.report({
node: declarationTemplateNode,
messageId: 'noCssColor',
});
}
});
}
}
}
/**
* @example
* <EuiCode css={() => ({ color: '#dd4040' })}>This is an example</EuiCode>
*/
if (
node.value.expression.type === 'FunctionExpression' ||
node.value.expression.type === 'ArrowFunctionExpression'
) {
let declarationPropertiesNode: TSESTree.Property[] = [];
if (node.value.expression.body.type === 'ObjectExpression') {
// @ts-expect-error
declarationPropertiesNode = node.value.expression.body.properties;
}
if (node.value.expression.body.type === 'BlockStatement') {
const functionReturnStatementNode = node.value.expression.body.body?.find((_node) => {
return _node.type === 'ReturnStatement';
});
if (!functionReturnStatementNode) {
return;
}
declarationPropertiesNode = // @ts-expect-error
(functionReturnStatementNode as TSESTree.ReturnStatement).argument?.properties;
}
if (!declarationPropertiesNode.length) {
return;
}
declarationPropertiesNode.forEach((property) => {
handleObjectProperties(context, node, property, {
loc: property.loc,
messageId: 'noCssColorSpecific',
data: {
// @ts-expect-error the key name is always present else this code will not execute
property: property.key.name,
},
});
});
return;
}
}
},
};
},
};

View file

@ -1,85 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { RuleTester } from 'eslint';
import { PreferCSSAttributeForEuiComponents } from './prefer_css_attribute_for_eui_components';
const tsTester = [
'@typescript-eslint/parser',
new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
ecmaFeatures: {
jsx: true,
},
},
}),
] as const;
const babelTester = [
'@babel/eslint-parser',
new RuleTester({
parser: require.resolve('@babel/eslint-parser'),
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
requireConfigFile: false,
babelOptions: {
presets: ['@kbn/babel-preset/node_preset'],
},
},
}),
] as const;
const invalid: RuleTester.InvalidTestCase[] = [
{
name: 'Prefer the JSX css attribute for EUI components',
filename: '/x-pack/plugins/observability_solution/observability/public/test_component.tsx',
code: `
import React from 'react';
function TestComponent() {
return (
<EuiCode style={{ color: '#dd4040' }}>This is a test</EuiCode>
)
}`,
errors: [
{
messageId: 'preferCSSAttributeForEuiComponents',
},
],
output: `
import React from 'react';
function TestComponent() {
return (
<EuiCode css={{ color: '#dd4040' }}>This is a test</EuiCode>
)
}`,
},
];
const valid: RuleTester.ValidTestCase[] = [
{
name: invalid[0].name,
filename: invalid[0].filename,
code: invalid[0].output as string,
},
];
for (const [name, tester] of [tsTester, babelTester]) {
describe(name, () => {
tester.run('@kbn/prefer_css_attribute_for_eui_components', PreferCSSAttributeForEuiComponents, {
valid,
invalid,
});
});
}

View file

@ -1,62 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { Rule } from 'eslint';
import type { TSESTree } from '@typescript-eslint/typescript-estree';
import type { Identifier, Node } from 'estree';
export const PreferCSSAttributeForEuiComponents: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
description: 'Prefer the JSX css attribute for EUI components',
category: 'Best Practices',
recommended: true,
},
messages: {
preferCSSAttributeForEuiComponents: 'Prefer the css attribute for EUI components',
},
fixable: 'code',
schema: [],
},
create(context) {
const isNamedEuiComponentRegex = /^Eui[A-Z]*/;
return {
JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
if (isNamedEuiComponentRegex.test((node.name as unknown as Identifier).name)) {
let styleAttrNode: TSESTree.JSXAttribute | undefined;
if (
// @ts-expect-error the returned result is somehow typed as a union of JSXAttribute and JSXAttributeSpread
(styleAttrNode = node.attributes.find(
(attr) => attr.type === 'JSXAttribute' && attr.name.name === 'style'
))
) {
context.report({
node: styleAttrNode?.parent! as Node,
messageId: 'preferCSSAttributeForEuiComponents',
fix(fixer) {
const cssAttr = node.attributes.find(
(attr) => attr.type === 'JSXAttribute' && attr.name.name === 'css'
);
if (cssAttr) {
return null;
}
return fixer.replaceTextRange(styleAttrNode?.name?.range!, 'css');
},
});
}
}
},
};
},
};

View file

@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": ["jest", "node"],
"lib": ["es2021"]
},
"include": ["**/*.ts"],
"exclude": ["target/**/*"],
"kbn_references": []
}

View file

@ -7,14 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
/* eslint-disable-next-line @kbn/eslint/module_migration */
import { default as v8Light } from '@elastic/eui/dist/eui_theme_amsterdam_light.json';
/* eslint-disable-next-line @kbn/eslint/module_migration */
import { default as v8Dark } from '@elastic/eui/dist/eui_theme_amsterdam_dark.json';
/* eslint-disable-next-line @kbn/eslint/module_migration */
import { default as borealisLight } from '@elastic/eui/dist/eui_theme_borealis_light.json';
/* eslint-disable-next-line @kbn/eslint/module_migration */
import { default as borealisDark } from '@elastic/eui/dist/eui_theme_borealis_dark.json';
const globals: any = typeof window === 'undefined' ? {} : window;

View file

@ -860,8 +860,6 @@
"@kbn/es-ui-shared-plugin/*": ["src/platform/plugins/shared/es_ui_shared/*"],
"@kbn/eslint-config": ["packages/kbn-eslint-config"],
"@kbn/eslint-config/*": ["packages/kbn-eslint-config/*"],
"@kbn/eslint-plugin-css": ["packages/kbn-eslint-plugin-css"],
"@kbn/eslint-plugin-css/*": ["packages/kbn-eslint-plugin-css/*"],
"@kbn/eslint-plugin-disable": ["packages/kbn-eslint-plugin-disable"],
"@kbn/eslint-plugin-disable/*": ["packages/kbn-eslint-plugin-disable/*"],
"@kbn/eslint-plugin-eslint": ["packages/kbn-eslint-plugin-eslint"],

View file

@ -61,7 +61,7 @@ const TacticComponent: React.FC<Props> = ({ detected, tactic }) => {
grow={false}
>
<div
// eslint-disable-next-line @kbn/css/no_css_color -- euiTheme.colors.danger is a string
// eslint-disable-next-line @elastic/eui/no-css-color -- euiTheme.colors.danger is a string
css={css`
background: transparent;
border: 2px solid ${color};
@ -74,7 +74,7 @@ const TacticComponent: React.FC<Props> = ({ detected, tactic }) => {
data-test-subj="innerCircle"
/>
<div
// eslint-disable-next-line @kbn/css/no_css_color -- euiTheme.colors.danger is a string
// eslint-disable-next-line @elastic/eui/no-css-color -- euiTheme.colors.danger is a string
css={css`
background: transparent;
border: 2px solid ${color};

View file

@ -2162,10 +2162,10 @@
semver "^7.6.3"
topojson-client "^3.1.0"
"@elastic/eslint-plugin-eui@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314"
integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ==
"@elastic/eslint-plugin-eui@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.1.1.tgz#2829b0f3cc750c582baa683c2f503594676ba08a"
integrity sha512-dCoXcCT8V1djwWrP0XSG0gXN5fYDtAw5aF88BPArVBbuCQSWPEBpR9avaWJ6uVjAsOteQ7tdwLMtzrvMKgKKIA==
"@elastic/eui-theme-borealis@0.1.0":
version "0.1.0"
@ -5410,10 +5410,6 @@
version "0.0.0"
uid ""
"@kbn/eslint-plugin-css@link:packages/kbn-eslint-plugin-css":
version "0.0.0"
uid ""
"@kbn/eslint-plugin-disable@link:packages/kbn-eslint-plugin-disable":
version "0.0.0"
uid ""