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 loading AsyncCodeEditor-фабрики.
  • В фабрике вызывает 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

1. JSONБазовый сценарий: json-редактор без шапки
tsx
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

2. YAMLYAML с включённой шапкой и кнопкой копирования
tsx
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

3. С шапкой и onCopyClickhasHeader + onCopyClick — продуктовая обвязка
tsx
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. Без фона

4. Без фонаbackground=false — для встраивания в карточки с собственным фоном
tsx
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-загрузка

5. Lazy-загрузкаLazyCodeEditor — Suspense + lazy-chunk с monaco-бандлом
tsx
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

PropsCodeEditorProps
PropTypeDefaultRequiredDescription
backgroundbooleannoВключение/отключение псевдобекграунда.
beforeMountBeforeMountnoSignature: 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"
classNamestringnoClass name for the editor container
data-test-idstringno
defaultLanguagestringnoDefault language of the current model
defaultPathstringnoDefault path of the current model Will be passed as the third argument to `.createModel` method `monaco.editor.createModel(..., ..., monaco.Uri.parse(defaultPath))`
defaultValuestringnoDefault value of the current model
hasHeaderbooleannoВключение/отключение шапки.
heightstring | number100%noHeight of the editor wrapper
jsonSchemaJsonSchemanoКонфигурация JSON-Schema, по которой monaco валидирует контент.
keepCurrentModelbooleanfalsenoIndicator whether to dispose the current model when the Editor is unmounted or not
languagestringnoLanguage of the current model Язык контента редактора. Schema-режим работает только для `json` и `yaml`.
linenumbernoThe line to jump on it
loadingReactNodeLoading...noThe loading screen before the editor will be mounted
onChangeOnChangenoSignature: 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Клик по кнопке копирования.
onMountOnMountnoSignature: 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"
onValidateOnValidatenoSignature: 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"
optionsIStandaloneEditorConstructionOptionsnoIStandaloneEditorConstructionOptions
overrideServicesIEditorOverrideServicesnoIEditorOverrideServices
pathstringnoPath 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 ошибке.
saveViewStatebooleannoIndicator whether to save the models' view states between model changes or not Defaults to true
showRowNumberbooleannoВключение/отключение колонки с номерами строк.
themestringnoИмя зарегистрированной monaco-темы из глобального реестра (`monaco.editor.defineTheme(name, …)`). По умолчанию подбирается автоматически по DS-теме провайдера: `'snack'` для светлой, `'snackDark'` для тёмной. Передавай вручную только если регистрируешь свою кастомную тему — иначе оставляй `undefined`.
themeNamestringnoИспользуется как trigger в effect deps для перепересчёта theme tokens при смене темы. Уникальное значение на каждую тему.
valuestringnoValue of the current model
widthstring | number100%noWidth of the editor wrapper
wrapperPropsobjectnoProps applied to the wrapper element

Types

CodeEditorProps

Storybook

Figma