CodeEditor
Тонкая обёртка над @monaco-editor/react с темой на токенах дизайн-системы, опциональной шапкой (language label + copy button) и фиксированными типографическими опциями. Поддерживает JSON/YAML-валидацию по JSON-Schema, async/lazy-варианты загрузки monaco-бандла и встраивание в продуктовые формы и карточки.
Когда использовать
- Просмотр и редактирование JSON/YAML конфигов прямо в продукте.
- Inline-редакторы с подсветкой синтаксиса.
- Просмотр логов/манифестов с кнопкой копирования всего содержимого.
Когда не нужен CodeEditor:
- Большие IDE-сценарии (мульти-файлы, расширения, дебаггер):
- используйте полноценный monaco-host или web-IDE.
- Однострочный ввод без подсветки:
- используйте
Input/Textareaиз соответствующих пакетов.
- используйте
Анатомия
Header (no default, опускается ⇒ скрыта)
hasHeader не имеет явного дефолта в API; если не передан — шапка не рендерится. Включение шапки добавляет:
- Имя языка (
language), отформатированное с заглавной буквы. - Кнопку копирования — копирует текущее значение
valueв clipboard и вызываетonCopyClick.
Background (default true)
true— поверх monaco-области рисуется лёгкий полупрозрачный фон, чтобы редактор не сливался с поверхностью.false— режим встраивания в карточки / модалки с собственным фоном.
Language
Любое значение, поддерживаемое monaco (json, yaml, typescript, javascript, markdown, shell, …). Для json и yaml дополнительно поддерживается jsonSchema — конфигурация диагностики и автокомплитов через JSON-Schema.
Loading
loading={true}— поверх редактора рендерится<Spinner />, само Monaco-поле скрыто. Использовать, когда контент ещё загружается из API.loading={<CustomNode />}— подменяет дефолтный<Spinner />на время инициализации monaco-бандла.
Row numbers (default true)
showRowNumber={false} скрывает колонку с номерами строк (mapping на monaco-опции lineNumbers: 'off', lineDecorationsWidth: 0, lineNumbersMinChars: 0).
Установка
pnpm add @cloud-ru/ds-code-editor
import { CodeEditor, LazyCodeEditor, AsyncCodeEditor } from '@cloud-ru/ds-code-editor'
Микрофронтовая интеграция
monaco-editor — крупный бандл (~3-5 MB после tree-shaking). В микрофронтовой архитектуре он должен грузиться один раз на уровне хост-приложения (хоста микрофронтов) и переиспользоваться всеми routed-MFE, иначе каждый микрофронт тянет свою копию.
Контракт реализован через глобальный реестр на window['__snack-monaco-editor-loader__']: хост регистрирует loader один раз, все MFE переиспользуют его без своей копии бандла.
Хост — однократная регистрация loader’а
В корневой конфигурации хоста (single-spa root-config / Module Federation host; вызывается до маунта любого MFE):
export function initCodeEditorConfig() {
if (typeof window === 'undefined') return
window['__snack-monaco-editor-loader__'] = {
loadMonaco: () => import('monaco-editor').then(m => m.default),
}
}
Хост содержит monaco-editor в собственных dependencies и в monaco-editor-webpack-plugin / Vite chunk-конфиге; MFE — не должны его содержать. Динамический import('monaco-editor') даёт отдельный chunk, который грузится только при первом обращении к loadMonaco().
MFE — потребитель
import { LazyCodeEditor } from '@cloud-ru/ds-code-editor'
export function ManifestPanel({ manifest }: { manifest: string }) {
return <LazyCodeEditor height='100%' value={manifest} language='json' options={{ readOnly: true }} />
}
LazyCodeEditor сам:
- Внутри
Suspenseпроисходит lazy loadingAsyncCodeEditor-фабрики. - В фабрике вызывает
loadMonacoEditor()— читаетwindow['__snack-monaco-editor-loader__']. - Если ключа нет (например, MFE отрендерился в standalone-режиме вне хоста) — fallback на дефолтный loader
@monaco-editor/react, который тянет бандл с jsDelivr CDN.
Если нужен собственный Suspense (например, общий лоадер MFE), используй AsyncCodeEditor напрямую:
import { Suspense, lazy } from 'react'
import { AsyncCodeEditor } from '@cloud-ru/ds-code-editor'
const CodeEditor = lazy(() => AsyncCodeEditor())
export function MyMfe() {
return (
<Suspense fallback={<MfeSpinner />}>
<CodeEditor language='json' value='' />
</Suspense>
)
}
Prefetch — preloadMonacoEditor()
Если заранее известно, что пользователь перейдёт к редактору (например, по клику в навигации) — предзагрузи бандл, чтобы Suspense-фолбэк не появлялся:
import { preloadMonacoEditor } from '@cloud-ru/ds-code-editor'
linkRef.current?.addEventListener('mouseenter', () => preloadMonacoEditor(), { once: true })
Если хост ещё не зарегистрировал loader (__snack-monaco-editor-loader__ отсутствует), preloadMonacoEditor() возвращает null — без ошибок.
SSR
loadMonacoEditor и preloadMonacoEditor имеют SSR-guard и в Node-окружении возвращают null. Сам CodeEditor / LazyCodeEditor под SSR не рендерится: monaco-editor обращается к window и document.fonts на module-init. В документации (Astro) demo-примеры монтируются через client:only='react' — в продуктовом SPA это не проблема, но при SSR-фреймворке (Next, Remix) оборачивай редактор в dynamic({ ssr: false })/аналог.
Self-hosted vs CDN
| Сценарий | Что делать |
|---|---|
| Хост + MFE | Зарегистрируй loader в хосте — import('monaco-editor'). MFE использует LazyCodeEditor. Один бандл на все MFE. |
| Standalone-приложение (без хоста) | Можешь либо зарегистрировать тот же loader локально, либо не делать ничего — @monaco-editor/react стянет бандл с jsDelivr CDN. Для продакшена предпочтительно self-hosted (offline, контроль версий, CSP). |
Прямой CodeEditor (не Lazy/Async) | Использует дефолтный loader @monaco-editor/react (CDN). Lazy/Async-варианты — единственный способ self-host’а через __snack-monaco-editor-loader__. |
1. JSON
import { CodeEditor } from '@cloud-ru/ds-code-editor';
import { useState } from 'react';
const INITIAL = `{
"name": "@cloud-ru/ds-code-editor",
"version": "0.0.0",
"language": "json"
}
`;
export function Json() {
const [value, setValue] = useState(INITIAL);
return <CodeEditor language='json' value={value} height={220} onChange={v => setValue(v ?? '')} />;
}2. YAML
import { CodeEditor } from '@cloud-ru/ds-code-editor';
import { useState } from 'react';
const INITIAL = `name: '@cloud-ru/ds-code-editor'
version: 0.0.0
language: yaml
`;
export function Yaml() {
const [value, setValue] = useState(INITIAL);
return <CodeEditor language='yaml' value={value} hasHeader height={220} onChange={v => setValue(v ?? '')} />;
}3. С шапкой и onCopyClick
import { CodeEditor } from '@cloud-ru/ds-code-editor';
import { useState } from 'react';
const INITIAL = `{
"copy": "me"
}
`;
export function WithHeader() {
const [value, setValue] = useState(INITIAL);
const [copiedAt, setCopiedAt] = useState<string | null>(null);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<CodeEditor
language='json'
value={value}
hasHeader
height={200}
onChange={v => setValue(v ?? '')}
onCopyClick={() => setCopiedAt(new Date().toLocaleTimeString())}
/>
<span>Last copy: {copiedAt ?? '—'}</span>
</div>
);
}4. Без фона
import { CodeEditor } from '@cloud-ru/ds-code-editor';
import { useState } from 'react';
import styles from './NoBackground.module.scss';
const INITIAL = `// transparent background, embedded into a card
const config = { mode: 'inline' }
`;
export function NoBackground() {
const [value, setValue] = useState(INITIAL);
return (
<div className={styles.surface}>
<CodeEditor
language='javascript'
value={value}
background={false}
height={220}
onChange={v => setValue(v ?? '')}
/>
</div>
);
}5. Lazy-загрузка
import { LazyCodeEditor } from '@cloud-ru/ds-code-editor';
import { useState } from 'react';
const INITIAL = `{
"loaded": "lazily"
}
`;
export function LazyLoaded() {
const [value, setValue] = useState(INITIAL);
return <LazyCodeEditor language='json' value={value} hasHeader height={220} onChange={v => setValue(v ?? '')} />;
}Props
Types
CodeEditorProps| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
background | boolean | — | no | Включение/отключение псевдобекграунда. |
beforeMount | BeforeMount | — | no | Signature: function(monaco: Monaco) => void An event is emitted before the editor is mounted It gets the monaco instance as a first argument Defaults to "noop" |
className | string | — | no | Class name for the editor container |
data-test-id | string | — | no | |
defaultLanguage | string | — | no | Default language of the current model |
defaultPath | string | — | no | Default path of the current model Will be passed as the third argument to `.createModel` method `monaco.editor.createModel(..., ..., monaco.Uri.parse(defaultPath))` |
defaultValue | string | — | no | Default value of the current model |
hasHeader | boolean | — | no | Включение/отключение шапки. |
height | string | number | 100% | no | Height of the editor wrapper |
jsonSchema | JsonSchema | — | no | Конфигурация JSON-Schema, по которой monaco валидирует контент. |
keepCurrentModel | boolean | false | no | Indicator whether to dispose the current model when the Editor is unmounted or not |
language | string | — | no | Language of the current model Язык контента редактора. Schema-режим работает только для `json` и `yaml`. |
line | number | — | no | The line to jump on it |
loading | ReactNode | Loading... | no | The loading screen before the editor will be mounted |
onChange | OnChange | — | no | Signature: function(value: string | undefined, ev: monaco.editor.IModelContentChangedEvent) => void An event is emitted when the content of the current model is changed |
onCopyClick | (() => void) | — | no | Клик по кнопке копирования. |
onMount | OnMount | — | no | Signature: function(editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => void An event is emitted when the editor is mounted It gets the editor instance as a first argument and the monaco instance as a second Defaults to "noop" |
onValidate | OnValidate | — | no | Signature: function(markers: monaco.editor.IMarker[]) => void An event is emitted when the content of the current model is changed and the current model markers are ready Defaults to "noop" |
options | IStandaloneEditorConstructionOptions | — | no | IStandaloneEditorConstructionOptions |
overrideServices | IEditorOverrideServices | — | no | IEditorOverrideServices |
path | string | — | no | Path of the current model Will be passed as the third argument to `.createModel` method `monaco.editor.createModel(..., ..., monaco.Uri.parse(defaultPath))` В schema-режиме путь модели управляется внутренне (нужен стабильный `path`, совпадающий с `fileMatch`), снаружи задать его нельзя. `never` запрещает передачу `path` на уровне типа — попытка приводит к compile-time ошибке. |
saveViewState | boolean | — | no | Indicator whether to save the models' view states between model changes or not Defaults to true |
showRowNumber | boolean | — | no | Включение/отключение колонки с номерами строк. |
theme | string | — | no | Имя зарегистрированной monaco-темы из глобального реестра (`monaco.editor.defineTheme(name, …)`). По умолчанию подбирается автоматически по DS-теме провайдера: `'snack'` для светлой, `'snackDark'` для тёмной. Передавай вручную только если регистрируешь свою кастомную тему — иначе оставляй `undefined`. |
themeName | string | — | no | Используется как trigger в effect deps для перепересчёта theme tokens при смене темы. Уникальное значение на каждую тему. |
value | string | — | no | Value of the current model |
width | string | number | 100% | no | Width of the editor wrapper |
wrapperProps | object | — | no | Props applied to the wrapper element |