Skip to content

Commit 7aaa04c

Browse files
committed
lazy load codeEditor component to reduce the frontend bundle size
1 parent 667c69b commit 7aaa04c

File tree

12 files changed

+181
-127
lines changed

12 files changed

+181
-127
lines changed

workspaces/extensions/.changeset/hot-trains-develop.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ Renamed plugins from marketplace to extensions
1010

1111
- Renamed all packages from `backstage-plugin-marketplace-*` to `backstage-plugin-extensions-*`
1212
- Updated all internal references, exports, and API endpoints
13+
- lazy load codeEditor to reduce the frontend plugin bundle size
1314
- Made catalog entities directory path configurable via `extensions.directory` in app-config.yaml with fallback to `extensions` and `marketplace` directories

workspaces/extensions/plugins/catalog-backend-module-extensions/README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,20 @@ The module looks for extension YAML files in a directory. You can configure a cu
4444
**Priority order:**
4545
4646
1. Configured directory (if specified in `extensions.directory`)
47-
2. `/extensions` directory (filesystem root)
48-
3. `/marketplace` directory (filesystem root)
47+
2. `opt/app-root/src/dynamic-plugins-root/extensions` directory
48+
3. `opt/app-root/src/dynamic-plugins-root/marketplace` directory
49+
4. `/extensions` directory (filesystem root)
50+
5. `/marketplace` directory (filesystem root)
4951

5052
**Example configuration:**
5153

5254
```yaml
5355
extensions:
5456
# Optional: Custom directory path for extension YAML files
5557
# Can be absolute path or relative to the working directory
56-
# If not specified, falls back to '/extensions' or '/marketplace' directories (filesystem root)
58+
# If not specified, falls back to:
59+
# - /extensions (filesystem root)
60+
# - /marketplace (filesystem root)
5761
directory: /path/to/custom/extensions
5862
```
5963

workspaces/extensions/plugins/catalog-backend-module-extensions/report.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { CatalogApi } from '@backstage/catalog-client';
1111
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
1212
import { CatalogProcessorCache } from '@backstage/plugin-catalog-node';
1313
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-node';
14+
import { Config } from '@backstage/config';
1415
import { DynamicPluginProvider } from '@backstage/backend-dynamic-feature-service';
1516
import { Entity } from '@backstage/catalog-model';
1617
import { EntityProvider } from '@backstage/plugin-catalog-node';
@@ -25,7 +26,7 @@ import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api';
2526

2627
// @public (undocumented)
2728
export abstract class BaseEntityProvider<T extends Entity> implements EntityProvider {
28-
constructor(taskRunner: SchedulerServiceTaskRunner);
29+
constructor(taskRunner: SchedulerServiceTaskRunner, config?: Config);
2930
// (undocumented)
3031
connect(connection: EntityProviderConnection): Promise<void>;
3132
// (undocumented)

workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
EntityProviderConnection,
2525
} from '@backstage/plugin-catalog-node';
2626
import { Config } from '@backstage/config';
27-
2827
import { readYamlFiles } from '../utils/file-utils';
2928
import { JsonFileData } from '../types';
3029
import path from 'path';
@@ -100,7 +99,12 @@ export abstract class BaseEntityProvider<T extends Entity>
10099

101100
/**
102101
* Gets the extensions directory path from config or falls back to hardcoded fallback directories
103-
* Priority: configured directory (if specified) -> 'extensions' -> 'marketplace'
102+
* Priority:
103+
* - configured directory (if specified)
104+
* - 'opt/app-root/src/dynamic-plugins-root/extensions'
105+
* - 'opt/app-root/src/dynamic-plugins-root/marketplace'
106+
* - '/extensions' (filesystem root)
107+
* - '/marketplace' (filesystem root)
104108
*/
105109
private getExtensionsDirectory(): string | null {
106110
if (this.config) {
@@ -122,22 +126,21 @@ export abstract class BaseEntityProvider<T extends Entity>
122126
}
123127
}
124128

125-
const firstFallback = this.resolveAndValidateDirectory(
129+
// Check fallback directories in priority order
130+
const fallbackDirectories = [
126131
BaseEntityProvider.EXTENSIONS_DIRECTORY,
127-
);
128-
if (firstFallback) {
129-
return firstFallback;
130-
}
131-
132-
const secondFallback = this.resolveAndValidateDirectory(
133132
BaseEntityProvider.DEPRECATED_MARKETPLACE_DIRECTORY,
134-
);
135-
if (secondFallback) {
136-
return secondFallback;
133+
];
134+
135+
for (const dir of fallbackDirectories) {
136+
const resolvedDir = this.resolveAndValidateDirectory(dir);
137+
if (resolvedDir) {
138+
return resolvedDir;
139+
}
137140
}
138141

139142
console.warn(
140-
`Extensions directory not found. Checked: configured directory, "${BaseEntityProvider.EXTENSIONS_DIRECTORY}", and "${BaseEntityProvider.DEPRECATED_MARKETPLACE_DIRECTORY}"`,
143+
`Extensions directory not found. Checked: configured directory "${BaseEntityProvider.EXTENSIONS_DIRECTORY}" and "${BaseEntityProvider.DEPRECATED_MARKETPLACE_DIRECTORY}"`,
141144
);
142145
return null;
143146
}

workspaces/extensions/plugins/extensions/src/components/CodeEditor.tsx

Lines changed: 10 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import type { ReactNode } from 'react';
18-
19-
import {
20-
createContext,
21-
useRef,
22-
useMemo,
23-
useContext,
24-
useState,
25-
useCallback,
26-
} from 'react';
17+
import { useState, useCallback } from 'react';
2718

2819
import { Progress } from '@backstage/core-components';
2920

@@ -33,84 +24,26 @@ import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded';
3324
import Typography from '@mui/material/Typography';
3425
import { useTheme } from '@mui/material/styles';
3526

36-
import Editor, { loader, OnChange, OnMount } from '@monaco-editor/react';
27+
import Editor, {
28+
loader,
29+
type OnChange,
30+
type OnMount,
31+
} from '@monaco-editor/react';
32+
33+
import type MonacoEditor from 'monaco-editor';
34+
import { useCodeEditor } from './CodeEditorContext';
3735

38-
// TODO: Load the CodeEditor or the pages that uses the CodeEditor lazy!
39-
// Currently manaco is loaded when the main extensions page is opened.
36+
// Import Monaco Editor modules - CodeEditor is lazy-loaded via CodeEditorCard
4037
import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution';
4138
// @ts-ignore
4239
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
43-
import type MonacoEditor from 'monaco-editor';
4440

4541
loader.config({ monaco: monacoEditor });
4642

4743
const defaultOptions: MonacoEditor.editor.IEditorConstructionOptions = {
4844
minimap: { enabled: false },
4945
};
5046

51-
interface CodeEditorContextValue {
52-
getEditor: () => MonacoEditor.editor.ICodeEditor | null;
53-
setEditor: (editor: MonacoEditor.editor.ICodeEditor) => void;
54-
55-
getSelection: () => MonacoEditor.Selection | null;
56-
setSelection: (editorSelection: MonacoEditor.Selection) => void;
57-
58-
getPosition: () => MonacoEditor.Position | null;
59-
setPosition: (cursorPosition: MonacoEditor.Position) => void;
60-
/** short for getEditor()?.getValue() */
61-
getValue: () => string | undefined;
62-
/** short for getEditor()?.setValue() and getEditor()?.focus() */
63-
setValue: (value: string, autoFocus?: boolean) => void;
64-
}
65-
66-
const CodeEditorContext = createContext<CodeEditorContextValue>(
67-
undefined as any as CodeEditorContextValue,
68-
);
69-
70-
export const CodeEditorContextProvider = (props: { children: ReactNode }) => {
71-
const editorRef = useRef<MonacoEditor.editor.ICodeEditor | null>(null);
72-
const contextValue = useMemo<CodeEditorContextValue>(
73-
() => ({
74-
getEditor: () => editorRef.current,
75-
setEditor: (editor: MonacoEditor.editor.ICodeEditor) => {
76-
editorRef.current = editor;
77-
},
78-
getPosition: () => editorRef.current?.getPosition() || null,
79-
setPosition: (cursorPosition: MonacoEditor.Position) => {
80-
editorRef.current?.setPosition(cursorPosition);
81-
},
82-
getSelection: () => editorRef.current?.getSelection() || null,
83-
setSelection: (editorSelection: MonacoEditor.Selection) => {
84-
editorRef.current?.setSelection(editorSelection);
85-
},
86-
getValue: () => editorRef.current?.getValue(),
87-
setValue: (value: string, autoFocus = true) => {
88-
editorRef.current?.setValue(value);
89-
if (autoFocus) {
90-
editorRef.current?.focus();
91-
}
92-
},
93-
}),
94-
[],
95-
);
96-
97-
return (
98-
<CodeEditorContext.Provider value={contextValue}>
99-
{props.children}
100-
</CodeEditorContext.Provider>
101-
);
102-
};
103-
104-
export const useCodeEditor = () => {
105-
const contextValue = useContext(CodeEditorContext);
106-
if (!contextValue) {
107-
throw new Error(
108-
'useCodeEditor must be used within a CodeEditorContextProvider',
109-
);
110-
}
111-
return contextValue;
112-
};
113-
11447
export interface CodeEditorProps {
11548
defaultLanguage: 'yaml'; // We enforce YAML for now
11649
defaultValue?: string;

workspaces/extensions/plugins/extensions/src/components/CodeEditorCard.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { lazy, Suspense } from 'react';
1718
import Grid from '@mui/material/Grid';
1819
import Card from '@mui/material/Card';
1920
import CardContent from '@mui/material/CardContent';
20-
import { CodeEditor } from './CodeEditor';
21+
import { Progress } from '@backstage/core-components';
22+
23+
// Lazy load CodeEditor to avoid loading Monaco Editor until needed
24+
const CodeEditor = lazy(() =>
25+
import('./CodeEditor').then(module => ({ default: module.CodeEditor })),
26+
);
2127

2228
export const CodeEditorCard = ({ onLoad }: { onLoad: () => void }) => {
2329
return (
@@ -45,7 +51,25 @@ export const CodeEditorCard = ({ onLoad }: { onLoad: () => void }) => {
4551
scrollbarWidth: 'thin',
4652
}}
4753
>
48-
<CodeEditor defaultLanguage="yaml" onLoaded={onLoad} />
54+
<Suspense
55+
fallback={
56+
<div
57+
style={{
58+
width: '100%',
59+
height: '100%',
60+
display: 'flex',
61+
alignItems: 'start',
62+
justifyContent: 'center',
63+
}}
64+
>
65+
<div style={{ width: '100%', height: '100px' }}>
66+
<Progress />
67+
</div>
68+
</div>
69+
}
70+
>
71+
<CodeEditor defaultLanguage="yaml" onLoaded={onLoad} />
72+
</Suspense>
4973
</CardContent>
5074
</Card>
5175
</Grid>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { ReactNode } from 'react';
18+
import { createContext, useRef, useMemo, useContext } from 'react';
19+
20+
import type MonacoEditor from 'monaco-editor';
21+
22+
interface CodeEditorContextValue {
23+
getEditor: () => MonacoEditor.editor.ICodeEditor | null;
24+
setEditor: (editor: MonacoEditor.editor.ICodeEditor) => void;
25+
26+
getSelection: () => MonacoEditor.Selection | null;
27+
setSelection: (editorSelection: MonacoEditor.Selection) => void;
28+
29+
getPosition: () => MonacoEditor.Position | null;
30+
setPosition: (cursorPosition: MonacoEditor.Position) => void;
31+
/** short for getEditor()?.getValue() */
32+
getValue: () => string | undefined;
33+
/** short for getEditor()?.setValue() and getEditor()?.focus() */
34+
setValue: (value: string, autoFocus?: boolean) => void;
35+
}
36+
37+
const CodeEditorContext = createContext<CodeEditorContextValue>(
38+
undefined as any as CodeEditorContextValue,
39+
);
40+
41+
export const CodeEditorContextProvider = (props: { children: ReactNode }) => {
42+
const editorRef = useRef<MonacoEditor.editor.ICodeEditor | null>(null);
43+
const contextValue = useMemo<CodeEditorContextValue>(
44+
() => ({
45+
getEditor: () => editorRef.current,
46+
setEditor: (editor: MonacoEditor.editor.ICodeEditor) => {
47+
editorRef.current = editor;
48+
},
49+
getPosition: () => editorRef.current?.getPosition() || null,
50+
setPosition: (cursorPosition: MonacoEditor.Position) => {
51+
editorRef.current?.setPosition(cursorPosition);
52+
},
53+
getSelection: () => editorRef.current?.getSelection() || null,
54+
setSelection: (editorSelection: MonacoEditor.Selection) => {
55+
editorRef.current?.setSelection(editorSelection);
56+
},
57+
getValue: () => editorRef.current?.getValue(),
58+
setValue: (value: string, autoFocus = true) => {
59+
editorRef.current?.setValue(value);
60+
if (autoFocus) {
61+
editorRef.current?.focus();
62+
}
63+
},
64+
}),
65+
[],
66+
);
67+
68+
return (
69+
<CodeEditorContext.Provider value={contextValue}>
70+
{props.children}
71+
</CodeEditorContext.Provider>
72+
);
73+
};
74+
75+
export const useCodeEditor = () => {
76+
const contextValue = useContext(CodeEditorContext);
77+
if (!contextValue) {
78+
throw new Error(
79+
'useCodeEditor must be used within a CodeEditorContextProvider',
80+
);
81+
}
82+
return contextValue;
83+
};

workspaces/extensions/plugins/extensions/src/components/ExtensionsPackageEditContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import Tooltip from '@mui/material/Tooltip';
4646

4747
import { packageInstallRouteRef } from '../routes';
4848

49-
import { CodeEditorContextProvider, useCodeEditor } from './CodeEditor';
49+
import { CodeEditorContextProvider, useCodeEditor } from './CodeEditorContext';
5050
import { useInstallPackage } from '../hooks/useInstallPackage';
5151
import { usePackage } from '../hooks/usePackage';
5252
import { usePackageConfig } from '../hooks/usePackageConfig';

workspaces/extensions/plugins/extensions/src/components/ExtensionsPluginInstallContent.test.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ jest.mock('../hooks/usePluginConfigurationPermissions', () => ({
5151
usePluginConfigurationPermissions: jest.fn(),
5252
}));
5353

54-
jest.mock('./CodeEditor', () => ({
54+
jest.mock('./CodeEditorContext', () => ({
5555
useCodeEditor: jest.fn(),
56+
CodeEditorContextProvider: ({ children }: any) => children,
5657
}));
5758

5859
jest.mock('../hooks/useExtensionsConfiguration', () => ({
@@ -73,17 +74,21 @@ jest.mock('@backstage/core-plugin-api', () => {
7374

7475
const mockCodeEditorSetValue = jest.fn();
7576

76-
jest.mock('./CodeEditor', () => ({
77-
CodeEditor: ({ onLoaded }: any) => {
78-
useEffect(() => {
79-
onLoaded?.();
80-
}, [onLoaded]);
81-
return <div>Code Editor Mock</div>;
82-
},
77+
jest.mock('./CodeEditorContext', () => ({
8378
useCodeEditor: () => ({
8479
setValue: mockCodeEditorSetValue,
8580
getValue: jest.fn(),
8681
}),
82+
CodeEditorContextProvider: ({ children }: any) => children,
83+
}));
84+
85+
jest.mock('./CodeEditorCard', () => ({
86+
CodeEditorCard: ({ onLoad }: any) => {
87+
useEffect(() => {
88+
onLoad?.();
89+
}, [onLoad]);
90+
return <div>Code Editor Card Mock</div>;
91+
},
8792
}));
8893

8994
beforeEach(() => {

0 commit comments

Comments
 (0)