Skip to content

Commit 438a3a1

Browse files
committed
lazy load codeEditor component to reduce the frontend bundle size
1 parent ac4be8d commit 438a3a1

File tree

11 files changed

+169
-107
lines changed

11 files changed

+169
-107
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: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,22 @@ 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+
# - opt/app-root/src/dynamic-plugins-root/extensions
60+
# - opt/app-root/src/dynamic-plugins-root/marketplace
61+
# - /extensions (filesystem root)
62+
# - /marketplace (filesystem root)
5763
directory: /path/to/custom/extensions
5864
```
5965

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: 22 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';
@@ -40,6 +39,10 @@ export abstract class BaseEntityProvider<T extends Entity>
4039
private taskRunner: SchedulerServiceTaskRunner;
4140
private config?: Config;
4241

42+
private static readonly DYNAMIC_PLUGINS_ROOT_EXTENSIONS_DIRECTORY =
43+
'/opt/app-root/src/dynamic-plugins-root/extensions';
44+
private static readonly DYNAMIC_PLUGINS_ROOT_MARKETPLACE_DIRECTORY =
45+
'/opt/app-root/src/dynamic-plugins-root/marketplace';
4346
private static readonly EXTENSIONS_DIRECTORY = '/extensions';
4447
private static readonly DEPRECATED_MARKETPLACE_DIRECTORY = '/marketplace';
4548

@@ -100,7 +103,12 @@ export abstract class BaseEntityProvider<T extends Entity>
100103

101104
/**
102105
* Gets the extensions directory path from config or falls back to hardcoded fallback directories
103-
* Priority: configured directory (if specified) -> 'extensions' -> 'marketplace'
106+
* Priority:
107+
* - configured directory (if specified)
108+
* - 'opt/app-root/src/dynamic-plugins-root/extensions'
109+
* - 'opt/app-root/src/dynamic-plugins-root/marketplace'
110+
* - '/extensions' (filesystem root)
111+
* - '/marketplace' (filesystem root)
104112
*/
105113
private getExtensionsDirectory(): string | null {
106114
if (this.config) {
@@ -122,22 +130,23 @@ export abstract class BaseEntityProvider<T extends Entity>
122130
}
123131
}
124132

125-
const firstFallback = this.resolveAndValidateDirectory(
133+
// Check fallback directories in priority order
134+
const fallbackDirectories = [
135+
BaseEntityProvider.DYNAMIC_PLUGINS_ROOT_EXTENSIONS_DIRECTORY,
136+
BaseEntityProvider.DYNAMIC_PLUGINS_ROOT_MARKETPLACE_DIRECTORY,
126137
BaseEntityProvider.EXTENSIONS_DIRECTORY,
127-
);
128-
if (firstFallback) {
129-
return firstFallback;
130-
}
131-
132-
const secondFallback = this.resolveAndValidateDirectory(
133138
BaseEntityProvider.DEPRECATED_MARKETPLACE_DIRECTORY,
134-
);
135-
if (secondFallback) {
136-
return secondFallback;
139+
];
140+
141+
for (const dir of fallbackDirectories) {
142+
const resolvedDir = this.resolveAndValidateDirectory(dir);
143+
if (resolvedDir) {
144+
return resolvedDir;
145+
}
137146
}
138147

139148
console.warn(
140-
`Extensions directory not found. Checked: configured directory, "${BaseEntityProvider.EXTENSIONS_DIRECTORY}", and "${BaseEntityProvider.DEPRECATED_MARKETPLACE_DIRECTORY}"`,
149+
`Extensions directory not found. Checked: configured directory, "${BaseEntityProvider.DYNAMIC_PLUGINS_ROOT_EXTENSIONS_DIRECTORY}", "${BaseEntityProvider.DYNAMIC_PLUGINS_ROOT_MARKETPLACE_DIRECTORY}", "${BaseEntityProvider.EXTENSIONS_DIRECTORY}", and "${BaseEntityProvider.DEPRECATED_MARKETPLACE_DIRECTORY}"`,
141150
);
142151
return null;
143152
}

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';

0 commit comments

Comments
 (0)