Skip to content

Commit 9493639

Browse files
fix(CodeModal): Switch over to lazy loading Monaco (#763)
Red Hat Developer Hub needs a smaller bundle size. They and we are unable to use Webpack plugins directly, so this is a solution that works independently of the Monaco Webpack plugin. Does add a loading state if the third-party dependency needs a moment. Consumers will need to add their own peer dependencies for monaco-editor and @monaco-editor/react.
1 parent 266b8aa commit 9493639

File tree

4 files changed

+99
-31
lines changed

4 files changed

+99
-31
lines changed

package-lock.json

Lines changed: 16 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/module/package.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,10 @@
3636
"@patternfly/react-code-editor": "^6.1.0",
3737
"@patternfly/react-core": "^6.1.0",
3838
"@patternfly/react-icons": "^6.1.0",
39-
"@patternfly/react-table": "^6.1.0",
4039
"@patternfly/react-styles": "^6.1.0",
40+
"@patternfly/react-table": "^6.1.0",
4141
"@segment/analytics-next": "^1.76.0",
4242
"clsx": "^2.1.0",
43-
"monaco-editor": "^0.54.0",
4443
"path-browserify": "^1.0.1",
4544
"posthog-js": "^1.194.4",
4645
"react-markdown": "^9.0.1",
@@ -52,9 +51,19 @@
5251
"unist-util-visit": "^5.0.0"
5352
},
5453
"peerDependencies": {
54+
"monaco-editor": "^0.54.0",
55+
"@monaco-editor/react": "^4.7.0",
5556
"react": "^18 || ^19",
5657
"react-dom": "^18 || ^19"
5758
},
59+
"peerDependenciesMeta": {
60+
"monaco-editor": {
61+
"optional": false
62+
},
63+
"@monaco-editor/react": {
64+
"optional": false
65+
}
66+
},
5867
"devDependencies": {
5968
"@octokit/rest": "^18.0.0",
6069
"@patternfly/documentation-framework": "6.28.9",

packages/module/src/CodeModal/CodeModal.tsx

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,34 @@
55
import type { FunctionComponent, MouseEvent } from 'react';
66
import { useState, useEffect, useRef } from 'react';
77
import path from 'path-browserify';
8-
import * as monaco from 'monaco-editor';
9-
import { loader } from '@monaco-editor/react';
108

119
// Import PatternFly components
1210
import { CodeEditor } from '@patternfly/react-code-editor';
1311
import {
12+
Bullseye,
1413
Button,
1514
getResizeObserver,
1615
ModalBody,
1716
ModalFooter,
1817
ModalHeader,
18+
Spinner,
1919
Stack,
2020
StackItem
2121
} from '@patternfly/react-core';
2222
import FileDetails, { extensionToLanguage } from '../FileDetails';
2323
import { ChatbotDisplayMode } from '../Chatbot';
2424
import ChatbotModal from '../ChatbotModal/ChatbotModal';
2525

26-
// Configure Monaco loader to use the npm package instead of CDN
27-
loader.config({ monaco });
26+
// Try to lazy load - some consumers need to be below a certain bundle size, but can't use the CDN and don't have webpack
27+
let monacoInstance: typeof import('monaco-editor') | null = null;
28+
const loadMonaco = async () => {
29+
if (!monacoInstance) {
30+
const [monaco, { loader }] = await Promise.all([import('monaco-editor'), import('@monaco-editor/react')]);
31+
monacoInstance = monaco;
32+
loader.config({ monaco });
33+
}
34+
return monacoInstance;
35+
};
2836

2937
export interface CodeModalProps {
3038
/** Class applied to code editor */
@@ -63,6 +71,8 @@ export interface CodeModalProps {
6371
modalBodyClassName?: string;
6472
/** Class applied to modal footer */
6573
modalFooterClassName?: string;
74+
/** Aria label applied to spinner when loading Monaco */
75+
spinnerAriaLabel?: string;
6676
}
6777

6878
export const CodeModal: FunctionComponent<CodeModalProps> = ({
@@ -84,13 +94,32 @@ export const CodeModal: FunctionComponent<CodeModalProps> = ({
8494
modalHeaderClassName,
8595
modalBodyClassName,
8696
modalFooterClassName,
97+
spinnerAriaLabel = 'Loading',
8798
...props
8899
}: CodeModalProps) => {
89100
const [newCode, setNewCode] = useState(code);
90-
const [editorInstance, setEditorInstance] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
101+
const [editorInstance, setEditorInstance] = useState<any>(null);
91102
const [isEditorReady, setIsEditorReady] = useState(false);
103+
const [isMonacoLoading, setIsMonacoLoading] = useState(false);
104+
const [isMonacoLoaded, setIsMonacoLoaded] = useState(false);
92105
const containerRef = useRef<HTMLDivElement>(null);
93106

107+
useEffect(() => {
108+
if (isModalOpen && !isMonacoLoaded && !isMonacoLoading) {
109+
setIsMonacoLoading(true);
110+
loadMonaco()
111+
.then(() => {
112+
setIsMonacoLoaded(true);
113+
setIsMonacoLoading(false);
114+
})
115+
.catch((error) => {
116+
// eslint-disable-next-line no-console
117+
console.error('Failed to load Monaco editor:', error);
118+
setIsMonacoLoading(false);
119+
});
120+
}
121+
}, [isModalOpen, isMonacoLoaded, isMonacoLoading]);
122+
94123
useEffect(() => {
95124
if (!isModalOpen || !isEditorReady || !editorInstance || !containerRef.current) {
96125
return;
@@ -148,6 +177,42 @@ export const CodeModal: FunctionComponent<CodeModalProps> = ({
148177
}
149178
};
150179

180+
const renderMonacoEditor = () => {
181+
if (isMonacoLoading) {
182+
return (
183+
<Bullseye>
184+
<Spinner aria-label={spinnerAriaLabel} />
185+
</Bullseye>
186+
);
187+
}
188+
if (isMonacoLoaded) {
189+
return (
190+
<CodeEditor
191+
isDarkTheme
192+
isLineNumbersVisible={isLineNumbersVisible}
193+
isLanguageLabelVisible
194+
isCopyEnabled={isCopyEnabled}
195+
isReadOnly={isReadOnly}
196+
code={newCode}
197+
language={extensionToLanguage[path.extname(fileName).slice(1)]}
198+
onEditorDidMount={onEditorDidMount}
199+
onCodeChange={onCodeChange}
200+
className={codeEditorClassName}
201+
isFullHeight
202+
options={{
203+
glyphMargin: false,
204+
folding: false,
205+
// prevents Monaco from handling resizing itself
206+
// was causing ResizeObserver issues
207+
automaticLayout: false
208+
}}
209+
{...props}
210+
/>
211+
);
212+
}
213+
return null;
214+
};
215+
151216
const modal = (
152217
<ChatbotModal
153218
isOpen={isModalOpen}
@@ -166,27 +231,7 @@ export const CodeModal: FunctionComponent<CodeModalProps> = ({
166231
<FileDetails fileName={fileName} />
167232
</StackItem>
168233
<div className="pf-v6-l-stack__item pf-chatbot__code-modal-editor" ref={containerRef}>
169-
<CodeEditor
170-
isDarkTheme
171-
isLineNumbersVisible={isLineNumbersVisible}
172-
isLanguageLabelVisible
173-
isCopyEnabled={isCopyEnabled}
174-
isReadOnly={isReadOnly}
175-
code={newCode}
176-
language={extensionToLanguage[path.extname(fileName).slice(1)]}
177-
onEditorDidMount={onEditorDidMount}
178-
onCodeChange={onCodeChange}
179-
className={codeEditorClassName}
180-
isFullHeight
181-
options={{
182-
glyphMargin: false,
183-
folding: false,
184-
// prevents Monaco from handling resizing itself
185-
// was causing ResizeObserver issues
186-
automaticLayout: false
187-
}}
188-
{...props}
189-
/>
234+
{renderMonacoEditor()}
190235
</div>
191236
</Stack>
192237
</ModalBody>

packages/module/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/* Basic Options */
66
// "incremental": true, /* Enable incremental compilation */
77
"target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
8-
"module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
8+
"module": "es2020" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
99
// "lib": [], /* Specify library files to be included in the compilation. */
1010
// "allowJs": true, /* Allow javascript files to be compiled. */
1111
// "checkJs": true, /* Report errors in .js files. */

0 commit comments

Comments
 (0)