mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
ES Lint rules for css-in-js declarations within Kibana (#200703)
## Summary Closes https://github.com/elastic/kibana-team/issues/1272 This PR adds implementation for eslint rules to help facilitate the migration away from SASS files to leveraging the design tokens EUI provides for styling. The introduced rules in this PR are as follows; - #### No CSS Color values Consider; ```tsx <EuiCode style={{ color: '#dd4040' }}>Hello World!</EuiCode> ``` this expression because it specifies the css color property, with a valid [CSS color value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value), when the aforementioned rule is enabled depending on the set report level set the user would get a feedback, see screenshot below; <img width="735" alt="Screenshot 2024-11-20 at 12 46 17" src="https://github.com/user-attachments/assets/d2f608dc-782c-4d83-88e6-92dfdd8f6101"> This rule also works for variables defined elsewhere in the code and referenced as a value to the style prop, see screenshot below; <img width="1658" alt="Screenshot 2024-11-26 at 13 29 45" src="https://github.com/user-attachments/assets/f8aadf6b-318b-4c6a-b7c9-bb44fb867b58"> feedback will also be provided when some variable that is a literal value is specified as a value for any earmarked property that should not specify literal values. <img width="1730" alt="Screenshot 2024-11-28 at 19 00 08" src="https://github.com/user-attachments/assets/bc3a8674-9469-4c7a-b0c9-7a2bfa7f08dc"> feedback will be provided for referencing a member prop of some object defined elsewhere as a value to any earmarked property that we have deemed to not specify literal values <img width="1676" alt="Screenshot 2024-11-29 at 11 36 44" src="https://github.com/user-attachments/assets/c4537fbf-b2d8-46bb-ad5f-8582e8c9a932"> Supports; - object values - object references - template literals - tagged templates This approach does not penalize variable declarations, only the usages of any said variable when it doesn't conform to expectation - #### Prefer CSS attributes for EUI components (optional) Consider; ```tsx <EuiCode style={{ someCSSProperty: 'propertyValue' }}>Hello World!</EuiCode> ``` A declaration like the one above, will be regarded as an error and can be fixed, when it's fixed it will be re-written as ```tsx <EuiCode css={{ someCSSProperty: 'propertyValue' }}>Hello World!</EuiCode> ``` <!-- ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_node:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... --> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
a5e25b2d5d
commit
7370cc712e
16 changed files with 1065 additions and 4 deletions
|
@ -764,7 +764,6 @@ module.exports = {
|
|||
],
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Jest specific rules
|
||||
*/
|
||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -337,6 +337,7 @@ packages/kbn-es-errors @elastic/kibana-core
|
|||
packages/kbn-es-query @elastic/kibana-data-discovery
|
||||
packages/kbn-es-types @elastic/kibana-core @elastic/obs-knowledge-team
|
||||
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-i18n @elastic/obs-knowledge-team @elastic/kibana-operations
|
||||
|
|
|
@ -1451,6 +1451,7 @@
|
|||
"@kbn/es": "link:packages/kbn-es",
|
||||
"@kbn/es-archiver": "link:packages/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-i18n": "link:packages/kbn-eslint-plugin-i18n",
|
||||
|
@ -1566,6 +1567,7 @@
|
|||
"@types/classnames": "^2.2.9",
|
||||
"@types/cli-progress": "^3.11.5",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/cssstyle": "^2.2.4",
|
||||
"@types/cytoscape": "^3.14.0",
|
||||
"@types/d3": "^3.5.43",
|
||||
"@types/d3-array": "^2.12.1",
|
||||
|
@ -1714,6 +1716,7 @@
|
|||
"css-loader": "^3.4.2",
|
||||
"cssnano": "^5.1.12",
|
||||
"cssnano-preset-default": "^5.2.12",
|
||||
"cssstyle": "^4.1.0",
|
||||
"csstype": "^3.0.2",
|
||||
"cypress": "13.15.2",
|
||||
"cypress-axe": "^1.5.0",
|
||||
|
|
|
@ -28,6 +28,7 @@ module.exports = {
|
|||
'@kbn/eslint-plugin-imports',
|
||||
'@kbn/eslint-plugin-telemetry',
|
||||
'@kbn/eslint-plugin-i18n',
|
||||
'@kbn/eslint-plugin-css',
|
||||
'eslint-plugin-depend',
|
||||
'prettier',
|
||||
],
|
||||
|
@ -332,6 +333,7 @@ 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',
|
||||
|
|
129
packages/kbn-eslint-plugin-css/README.mdx
Normal file
129
packages/kbn-eslint-plugin-css/README.mdx
Normal file
|
@ -0,0 +1,129 @@
|
|||
---
|
||||
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.
|
||||
|
20
packages/kbn-eslint-plugin-css/index.ts
Normal file
20
packages/kbn-eslint-plugin-css/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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,
|
||||
};
|
14
packages/kbn-eslint-plugin-css/jest.config.js
Normal file
14
packages/kbn-eslint-plugin-css/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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'],
|
||||
};
|
6
packages/kbn-eslint-plugin-css/kibana.jsonc
Normal file
6
packages/kbn-eslint-plugin-css/kibana.jsonc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/eslint-plugin-css",
|
||||
"devOnly": true,
|
||||
"owner": "@elastic/appex-sharedux"
|
||||
}
|
6
packages/kbn-eslint-plugin-css/package.json
Normal file
6
packages/kbn-eslint-plugin-css/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"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"
|
||||
}
|
249
packages/kbn-eslint-plugin-css/src/rules/no_css_color.test.ts
Normal file
249
packages/kbn-eslint-plugin-css/src/rules/no_css_color.test.ts
Normal file
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* 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[] = [];
|
||||
|
||||
for (const [name, tester] of [tsTester, babelTester]) {
|
||||
describe(name, () => {
|
||||
tester.run('@kbn/no_css_color', NoCssColor, {
|
||||
valid,
|
||||
invalid,
|
||||
});
|
||||
});
|
||||
}
|
453
packages/kbn-eslint-plugin-css/src/rules/no_css_color.ts
Normal file
453
packages/kbn-eslint-plugin-css/src/rules/no_css_color.ts
Normal file
|
@ -0,0 +1,453 @@
|
|||
/*
|
||||
* 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 specifics an index signature of number, alongside other valid CSS properties
|
||||
style[property] = 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 a string is used to define a color to mark it as invalid
|
||||
// @ts-ignore the types for this packages specifics an index signature of number, alongside other valid CSS properties
|
||||
return typeof style[resolvedColorProperty] === 'string';
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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,
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
11
packages/kbn-eslint-plugin-css/tsconfig.json
Normal file
11
packages/kbn-eslint-plugin-css/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": ["jest", "node"],
|
||||
"lib": ["es2021"]
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["target/**/*"],
|
||||
"kbn_references": []
|
||||
}
|
|
@ -850,6 +850,8 @@
|
|||
"@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"],
|
||||
|
@ -2080,9 +2082,7 @@
|
|||
"@kbn/zod-helpers/*": ["packages/kbn-zod-helpers/*"],
|
||||
// END AUTOMATED PACKAGE LISTING
|
||||
// Allows for importing from `kibana` package for the exported types.
|
||||
"@emotion/core": [
|
||||
"typings/@emotion"
|
||||
]
|
||||
"@emotion/core": ["typings/@emotion"]
|
||||
},
|
||||
// Support .tsx files and transform JSX into calls to React.createElement
|
||||
"jsx": "react",
|
||||
|
|
21
yarn.lock
21
yarn.lock
|
@ -5517,6 +5517,10 @@
|
|||
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 ""
|
||||
|
@ -11345,6 +11349,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78"
|
||||
integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==
|
||||
|
||||
"@types/cssstyle@^2.2.4":
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/cssstyle/-/cssstyle-2.2.4.tgz#3d333ab9f8e6c40183ad1d6ebeebfcb8da2bfe4b"
|
||||
integrity sha512-FTGMeuHZtLB7hRm+NGvOLZElslR1UkKvZmEmFevOZe/e7Av0nFleka1s8ZwoX+QvbJ2y7r9NDZXIzyqpRWDJXQ==
|
||||
|
||||
"@types/cytoscape@^3.14.0":
|
||||
version "3.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.14.0.tgz#346b5430a7a1533784bcf44fcbe6c5255b948d36"
|
||||
|
@ -16338,6 +16347,13 @@ cssstyle@^2.3.0:
|
|||
dependencies:
|
||||
cssom "~0.3.6"
|
||||
|
||||
cssstyle@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.1.0.tgz#161faee382af1bafadb6d3867a92a19bcb4aea70"
|
||||
integrity sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==
|
||||
dependencies:
|
||||
rrweb-cssom "^0.7.1"
|
||||
|
||||
csstype@3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
|
@ -28894,6 +28910,11 @@ robust-predicates@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a"
|
||||
integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==
|
||||
|
||||
rrweb-cssom@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b"
|
||||
integrity sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==
|
||||
|
||||
rst-selector-parser@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue