Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 4 additions & 36 deletions packages/x/components/code-highlighter/CodeHighlighter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { clsx } from 'clsx';
import React, { lazy, Suspense } from 'react';
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
import useXComponentConfig from '../_util/hooks/use-x-component-config';
import Actions from '../actions';
Expand All @@ -16,38 +16,9 @@ const customOneLight = {
},
};

// Module-level cache for loaded language highlighters
const highlighterCache = new Map<string, React.LazyExoticComponent<React.ComponentType<any>>>();
// Full Prism highlighter (cached, loaded on demand)
let FullPrismHighlighter: React.LazyExoticComponent<React.ComponentType<any>> | null = null;

const getAsyncHighlighter = (lang: string) => {
if (!highlighterCache.has(lang)) {
const LazyHighlighter = lazy(async () => {
try {
const langModule = await import(
`react-syntax-highlighter/dist/esm/languages/prism/${lang}`
);
SyntaxHighlighter.registerLanguage(lang, langModule.default);
} catch (error) {
console.warn(`[CodeHighlighter] Failed to load language: ${lang}`, error);
}
return {
default: ({
children,
...rest
}: { children: string } & CodeHighlighterProps['highlightProps']) => (
<SyntaxHighlighter language={lang} {...rest}>
{children}
</SyntaxHighlighter>
),
};
});
highlighterCache.set(lang, LazyHighlighter);
}
return highlighterCache.get(lang)!;
};

const getFullPrismHighlighter = () => {
if (!FullPrismHighlighter) {
FullPrismHighlighter = lazy(() =>
Expand Down Expand Up @@ -81,13 +52,10 @@ const CodeHighlighter = React.forwardRef<HTMLDivElement, CodeHighlighterProps>((
const contextConfig = useXComponentConfig('codeHighlighter');

// Get the appropriate highlighter component
// - prismLightMode = true (default): Use PrismLight with async language loading
// - prismLightMode = true (default): Use PrismAsyncLight, which loads AND registers the
// grammar for `lang` on demand through a statically analyzable loader map
// - prismLightMode = false: Use full Prism (all languages included)
const Highlighter = prismLightMode
? lang
? getAsyncHighlighter(lang)
: SyntaxHighlighter
: getFullPrismHighlighter();
const Highlighter = prismLightMode ? SyntaxHighlighter : getFullPrismHighlighter();

// ============================ Early Returns ============================
if (!children) {
Expand Down
30 changes: 4 additions & 26 deletions packages/x/components/code-highlighter/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,6 @@ jest.mock('../../mermaid', () => ({
default: () => <div data-testid="mock-mermaid">Mermaid Diagram</div>,
}));

// Mock react-syntax-highlighter
jest.mock('react-syntax-highlighter/dist/esm/languages/prism/typescript', () => ({
__esModule: true,
default: () => null,
}));

// Mock a language that doesn't exist to test the catch block
jest.mock(
'react-syntax-highlighter/dist/esm/languages/prism/nonexistent-lang',
() => {
throw new Error('Module not found');
},
{ virtual: true },
);

// Spy on console.warn
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});

Expand Down Expand Up @@ -480,26 +465,19 @@ describe('CodeHighlighter', () => {
});

describe('language loading error handling', () => {
it('should handle language import failure gracefully', async () => {
// Use a non-existent language that will fail to import
// This tests the catch block in getAsyncHighlighter (line 30)
it('should handle an unsupported language gracefully', async () => {
// PrismAsyncLight normalizes unsupported languages to `text` instead of throwing
const { container } = render(
<CodeHighlighter lang="nonexistent-lang">{`console.log("test");`}</CodeHighlighter>,
);

// Should still render the code even if language import fails
// Should still render the code even if the language is not supported
await waitFor(() => {
expect(container.querySelector('pre')).toBeInTheDocument();
});

// Should have logged a warning about the failed language import
expect(consoleWarnSpy).toHaveBeenCalledWith(
'[CodeHighlighter] Failed to load language: nonexistent-lang',
expect.any(Error),
);
});

it('should render code fallback when language import fails', async () => {
it('should render code fallback when the language is unsupported', async () => {
// Use a non-existent language
const { container } = render(
<CodeHighlighter lang="nonexistent-lang">{`const x = 42;`}</CodeHighlighter>,
Expand Down
2 changes: 1 addition & 1 deletion packages/x/components/folder/FilePreview.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Empty, Flex, Spin } from 'antd';
import { clsx } from 'clsx';
import React from 'react';
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
import useXComponentConfig from '../_util/hooks/use-x-component-config';
import ActionsCopy from '../actions/ActionsCopy';
Expand Down
Loading