diff --git a/.buildkite/scripts/steps/checks/quick_checks.txt b/.buildkite/scripts/steps/checks/quick_checks.txt index 9bd9224673905..93c192d683dfc 100644 --- a/.buildkite/scripts/steps/checks/quick_checks.txt +++ b/.buildkite/scripts/steps/checks/quick_checks.txt @@ -17,3 +17,4 @@ .buildkite/scripts/steps/checks/prettier_topology.sh .buildkite/scripts/steps/checks/renovate.sh .buildkite/scripts/steps/checks/native_modules.sh +.buildkite/scripts/steps/checks/styled_components_mapping.sh diff --git a/.buildkite/scripts/steps/checks/styled_components_mapping.sh b/.buildkite/scripts/steps/checks/styled_components_mapping.sh new file mode 100644 index 0000000000000..2815bf41180b5 --- /dev/null +++ b/.buildkite/scripts/steps/checks/styled_components_mapping.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +echo --- Check styled-components mapping +cmd="node scripts/styled_components_mapping" + +eval "$cmd" +check_for_changed_files "$cmd" true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3074d95d25077..0362a5210fd45 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1041,6 +1041,7 @@ src/platform/packages/shared/kbn-xstate-utils @elastic/obs-ux-logs-team packages/kbn-yarn-lock-validator @elastic/kibana-operations src/platform/packages/shared/kbn-zod @elastic/kibana-core src/platform/packages/shared/kbn-zod-helpers @elastic/security-detection-rule-management +packages/kbn-styled-components-mapping-cli @elastic/kibana-operations @elastic/eui-team #### ## Everything below this line overrides the default assignments for each package. ## Items lower in the file have higher precedence: diff --git a/package.json b/package.json index 4a3a0527a4800..eb52a1fb53d90 100644 --- a/package.json +++ b/package.json @@ -1504,6 +1504,7 @@ "@kbn/spec-to-console": "link:packages/kbn-spec-to-console", "@kbn/stdio-dev-helpers": "link:packages/kbn-stdio-dev-helpers", "@kbn/storybook": "link:packages/kbn-storybook", + "@kbn/styled-components-mapping-cli": "link:packages/kbn-styled-components-mapping-cli", "@kbn/synthetics-e2e": "link:x-pack/solutions/observability/plugins/synthetics/e2e", "@kbn/synthetics-private-location": "link:x-pack/packages/kbn-synthetics-private-location", "@kbn/telemetry-tools": "link:packages/kbn-telemetry-tools", diff --git a/packages/kbn-styled-components-mapping-cli/README.md b/packages/kbn-styled-components-mapping-cli/README.md new file mode 100644 index 0000000000000..42ed1cde6149e --- /dev/null +++ b/packages/kbn-styled-components-mapping-cli/README.md @@ -0,0 +1,10 @@ +# @kbn/styled-components-mapping-cli + +A helper tool that looks up components using `styled-components` and generates +a mapping file for Babel to help with proper stylesheet loading. + +## Usage + +```shell +node scripts/styled_components_mapping +``` diff --git a/packages/kbn-styled-components-mapping-cli/jest.config.js b/packages/kbn-styled-components-mapping-cli/jest.config.js new file mode 100644 index 0000000000000..f4b92ce3bcd3f --- /dev/null +++ b/packages/kbn-styled-components-mapping-cli/jest.config.js @@ -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/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-styled-components-mapping-cli'], +}; diff --git a/packages/kbn-styled-components-mapping-cli/kibana.jsonc b/packages/kbn-styled-components-mapping-cli/kibana.jsonc new file mode 100644 index 0000000000000..282032b252dad --- /dev/null +++ b/packages/kbn-styled-components-mapping-cli/kibana.jsonc @@ -0,0 +1,6 @@ +{ + "type": "shared-common", + "id": "@kbn/styled-components-mapping-cli", + "owner": ["@elastic/kibana-operations", "@elastic/eui-team"], + "devOnly": true +} diff --git a/packages/kbn-styled-components-mapping-cli/package.json b/packages/kbn-styled-components-mapping-cli/package.json new file mode 100644 index 0000000000000..43f8f2d625565 --- /dev/null +++ b/packages/kbn-styled-components-mapping-cli/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/styled-components-mapping-cli", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0", + "main": "./src/run_styled_components_mapping_cli.ts" +} \ No newline at end of file diff --git a/packages/kbn-styled-components-mapping-cli/src/find_files.ts b/packages/kbn-styled-components-mapping-cli/src/find_files.ts new file mode 100644 index 0000000000000..a984c6a84e04e --- /dev/null +++ b/packages/kbn-styled-components-mapping-cli/src/find_files.ts @@ -0,0 +1,79 @@ +/* + * 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 * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + +const SOURCE_DIRS = ['x-pack', 'src', 'packages']; +const SOURCE_FILE_REGEX = /(^.?|\.[^d]|[^.]d|[^.][^d])\.tsx?$/; +const STYLED_COMPONENTS_IMPORT_REGEX = + /import\s+(?:{[^{}]+}|.*?)\s*(?:from)?\s*['"](styled-components)['"]/; +const EMOTION_STYLED_IMPORT_REGEX = + /import\s+(?:{[^{}]+}|.*?)\s*(?:from)?\s*['"](@emotion\/styled)['"]/; + +export interface Meta { + path: string; + usesStyledComponents: boolean; + usesOnlyStyledComponents?: boolean; + files?: Meta[]; +} + +const walkDirectory = async (dirPath: string): Promise => { + const files: Meta[] = []; + let usesStyledComponents = false; + let usesOnlyStyledComponents = true; + + for (const file of await fs.readdir(dirPath, { withFileTypes: true })) { + const fullPath = path.join(file.path, file.name); + + if (file.isDirectory()) { + const meta = await walkDirectory(fullPath); + if (meta.usesStyledComponents) { + usesStyledComponents = true; + } + if (usesOnlyStyledComponents && !meta.usesOnlyStyledComponents) { + usesOnlyStyledComponents = false; + } + files.push(meta); + continue; + } + + if (!SOURCE_FILE_REGEX.test(file.name) || !file.isFile()) { + continue; + } + + const meta: Meta = { + path: fullPath, + usesStyledComponents: false, + }; + + const contents = await fs.readFile(fullPath, 'utf8'); + const usesEmotionStyled = EMOTION_STYLED_IMPORT_REGEX.test(contents); + meta.usesStyledComponents = STYLED_COMPONENTS_IMPORT_REGEX.test(contents); + + if (usesEmotionStyled) { + usesOnlyStyledComponents = false; + } + + if (usesEmotionStyled || meta.usesStyledComponents) { + files.push(meta); + } + } + + return { + path: dirPath, + files, + usesStyledComponents, + usesOnlyStyledComponents: usesStyledComponents && usesOnlyStyledComponents, + }; +}; + +export const findFiles = async (rootDirPath: string) => { + return Promise.all(SOURCE_DIRS.map((dir) => walkDirectory(path.join(rootDirPath, dir)))); +}; diff --git a/packages/kbn-styled-components-mapping-cli/src/generate_regex_array.ts b/packages/kbn-styled-components-mapping-cli/src/generate_regex_array.ts new file mode 100644 index 0000000000000..529749edc4371 --- /dev/null +++ b/packages/kbn-styled-components-mapping-cli/src/generate_regex_array.ts @@ -0,0 +1,35 @@ +/* + * 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 * as path from 'node:path'; +import type { Meta } from './find_files'; + +export const pathToRegexString = (modulePath: string): string => { + modulePath = modulePath.replace(/[\\\/]/g, '[\\/\\\\]'); + + return `/${modulePath}/`; +}; + +export const generateRegexStringArray = (files: Meta[], rootDirPath: string): string[] => { + const array: string[] = []; + + for (const meta of files) { + if (meta.files) { + if (meta.usesOnlyStyledComponents) { + array.push(pathToRegexString(path.relative(rootDirPath, meta.path))); + } else { + array.push(...generateRegexStringArray(meta.files, rootDirPath)); + } + } else if (meta.usesStyledComponents) { + array.push(pathToRegexString(path.relative(rootDirPath, meta.path))); + } + } + + return array; +}; diff --git a/packages/kbn-styled-components-mapping-cli/src/run_styled_components_mapping_cli.ts b/packages/kbn-styled-components-mapping-cli/src/run_styled_components_mapping_cli.ts new file mode 100644 index 0000000000000..cfad8da6446fa --- /dev/null +++ b/packages/kbn-styled-components-mapping-cli/src/run_styled_components_mapping_cli.ts @@ -0,0 +1,40 @@ +/* + * 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 * as path from 'node:path'; +import { run } from '@kbn/dev-cli-runner'; +import { createFailError } from '@kbn/dev-cli-errors'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { findFiles } from './find_files'; +import { generateRegexStringArray } from './generate_regex_array'; +import { updateFile } from './update_file'; + +const mappingFilePath = 'packages/kbn-babel-preset/styled_components_files.js'; +const mappingFileAbsolutePath = path.join(REPO_ROOT, mappingFilePath); + +run( + async ({ log }) => { + log.info(`Looking for source files importing 'styled-components'...`); + + const files = await findFiles(REPO_ROOT); + const regexStringArray = generateRegexStringArray(files, REPO_ROOT); + + try { + await updateFile(mappingFileAbsolutePath, regexStringArray); + } catch (err) { + createFailError(err); + } + + log.info(`Generated ${mappingFilePath} with ${regexStringArray.length} regex expressions`); + }, + { + usage: `node scripts/styled_components_mapping`, + description: 'Update styled-components babel mapping when converting styles to Emotion', + } +); diff --git a/packages/kbn-styled-components-mapping-cli/src/update_file.ts b/packages/kbn-styled-components-mapping-cli/src/update_file.ts new file mode 100644 index 0000000000000..5e995e43741d2 --- /dev/null +++ b/packages/kbn-styled-components-mapping-cli/src/update_file.ts @@ -0,0 +1,48 @@ +/* + * 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 * as fs from 'node:fs/promises'; + +const babelFilePrefix = `/* + * 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 = { + /** + * Synchronized list of all source files that use styled-components. + * Please keep this list up-to-date when converting component styles + * from styled-components to Emotion. + * + * Babel's MatchPattern can be a regex or a string which follows standard + * Node.js path logic as described here: + * https://babeljs.io/docs/options#matchpattern + * + * Used by \`kbn-babel-preset\` and \`kbn-eslint-config\`. + */ + USES_STYLED_COMPONENTS: [ + /packages[\\/\\\\]kbn-ui-shared-deps-npm[\\/\\\\]/, + /packages[\\/\\\\]kbn-ui-shared-deps-src[\\/\\\\]/, +`; + +const babelFileSuffix = ` ], +}; +`; + +export const updateFile = async (filePath: string, regexStringArray: string[]) => { + const contents = `${babelFilePrefix}\n ${regexStringArray.join( + ',\n ' + )},\n${babelFileSuffix}`; + + return fs.writeFile(filePath, contents); +}; diff --git a/packages/kbn-styled-components-mapping-cli/tsconfig.json b/packages/kbn-styled-components-mapping-cli/tsconfig.json new file mode 100644 index 0000000000000..1e44316610f69 --- /dev/null +++ b/packages/kbn-styled-components-mapping-cli/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/dev-cli-runner", + "@kbn/dev-cli-errors", + "@kbn/repo-info", + ] +} diff --git a/scripts/styled_components_mapping.js b/scripts/styled_components_mapping.js new file mode 100644 index 0000000000000..770155b7882f7 --- /dev/null +++ b/scripts/styled_components_mapping.js @@ -0,0 +1,11 @@ +/* + * 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". + */ + +require('../src/setup_node_env'); +require('@kbn/styled-components-mapping-cli'); diff --git a/tsconfig.base.json b/tsconfig.base.json index c3375458959b7..a3c7efe962b6b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1878,6 +1878,8 @@ "@kbn/streams-plugin/*": ["x-pack/solutions/observability/plugins/streams/*"], "@kbn/streams-schema": ["x-pack/packages/kbn-streams-schema"], "@kbn/streams-schema/*": ["x-pack/packages/kbn-streams-schema/*"], + "@kbn/styled-components-mapping-cli": ["packages/kbn-styled-components-mapping-cli"], + "@kbn/styled-components-mapping-cli/*": ["packages/kbn-styled-components-mapping-cli/*"], "@kbn/synthetics-e2e": ["x-pack/solutions/observability/plugins/synthetics/e2e"], "@kbn/synthetics-e2e/*": ["x-pack/solutions/observability/plugins/synthetics/e2e/*"], "@kbn/synthetics-plugin": ["x-pack/solutions/observability/plugins/synthetics"], diff --git a/yarn.lock b/yarn.lock index 8e2144b587caf..5c85f57ae1f12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7608,6 +7608,10 @@ version "0.0.0" uid "" +"@kbn/styled-components-mapping-cli@link:packages/kbn-styled-components-mapping-cli": + version "0.0.0" + uid "" + "@kbn/synthetics-e2e@link:x-pack/solutions/observability/plugins/synthetics/e2e": version "0.0.0" uid ""