From 2deaa491205e4bcdc9ca0757eda13f37e27d4d39 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 20 Apr 2026 17:59:45 +0100 Subject: [PATCH] fix(ui): replace react-markdown with inline parser to fix ESM Jest failures react-markdown v8+ is ESM-only and breaks Jest in Node-role packages that transitively import @backstage/ui via core-app-api. Since the Header description only needs inline link support, a small regex-based parser is sufficient and avoids the ESM dependency entirely. Signed-off-by: Charles de Dreuille --- packages/ui/package.json | 1 - packages/ui/src/components/Header/Header.tsx | 57 +++++++++++++------- yarn.lock | 1 - 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 242d15036c..1b17bc8744 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -52,7 +52,6 @@ "clsx": "^2.1.1", "react-aria": "~3.48.0", "react-aria-components": "~1.17.0", - "react-markdown": "^8.0.0", "react-stately": "~3.46.0", "use-sync-external-store": "^1.4.0" }, diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx index 2518a6716d..9bd8bcdeab 100644 --- a/packages/ui/src/components/Header/Header.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -22,7 +22,40 @@ import { useDefinition } from '../../hooks/useDefinition'; import { HeaderDefinition } from './definition'; import { Link } from '../Link'; import { Fragment } from 'react/jsx-runtime'; -import ReactMarkdown from 'react-markdown'; + +const INLINE_LINK_RE = /\[([^\]]+)\]\(([^)]+)\)/g; + +/** + * Renders a plain-text string that may contain inline Markdown links of the + * form `[label](href)` as an array of React nodes (strings and Link elements). + * + * We intentionally avoid `react-markdown` here: that package is ESM-only + * (v8+), which breaks Jest in Node-role packages that transitively import + * `@backstage/ui` (e.g. via `core-app-api`). Since the Header description only + * needs inline link support, a small regex-based parser is sufficient and keeps + * this package free of ESM dependencies. + */ +function renderInlineMarkdown(text: string): React.ReactNode[] { + const parts: React.ReactNode[] = []; + let last = 0; + let match: RegExpExecArray | null; + INLINE_LINK_RE.lastIndex = 0; + while ((match = INLINE_LINK_RE.exec(text)) !== null) { + if (match.index > last) { + parts.push(text.slice(last, match.index)); + } + parts.push( + + {match[1]} + , + ); + last = match.index + match[0].length; + } + if (last < text.length) { + parts.push(text.slice(last)); + } + return parts; +} /** * A secondary header with title, breadcrumbs, tabs, and actions. @@ -94,25 +127,13 @@ export const Header = (props: HeaderProps) => {
{customActions}
{description && ( - ( - - {children} - - ), - a: ({ href, children }) => ( - - {children} - - ), - }} > - {description} - + {renderInlineMarkdown(description)} + )} {metadata && metadata.length > 0 && (
diff --git a/yarn.lock b/yarn.lock index 7726a28c2a..6802845bac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7987,7 +7987,6 @@ __metadata: react-aria: "npm:~3.48.0" react-aria-components: "npm:~1.17.0" react-dom: "npm:^18.0.2" - react-markdown: "npm:^8.0.0" react-router-dom: "npm:^6.30.2" react-stately: "npm:~3.46.0" storybook: "npm:^10.3.3"