Skip to main content

Архитектура Builder-web — подробная версия

Расширенное описание внутреннего устройства визуального редактора, потоков данных, состояния, валидации, превью, expression‑DSL, подсветки ошибок и системы моков для источников данных.


🎯 Цель страницы

Эта страница подробно объясняет архитектуру @lowcode/builder-web, включая:

  • структуру каталогов и роль каждого модуля;
  • устройство центрального состояния редактора (EditorState);
  • формирование и обновление DSL‑дерева;
  • работу панелей PropertiesPanel / StatePanel / EventHandlersPanel / DataSourcesPanel;
  • механизм expression‑валидации (syntax + semantic);
  • превью‑pipeline (DSL → AST → TSX → JS → runtime-core);
  • механизм подсветки ошибок и связь Canvas ↔ PreviewPanel;
  • интеграцию с @lowcode/runtime-core и devtools‑событиями;
  • поддержку моков (mock) для DataSource в DSL и превью;
  • принципы проектирования всего редактора.

Документ предназначен как глубокое архитектурное описание.


1. Общая роль Builder-web

builder-web — это визуальная IDE для работы с DSL‑моделью приложения. Он объединяет:

  • графический редактор UI (Canvas);
  • редактор свойств (PropertiesPanel v2);
  • редактор состояния (StatePanel);
  • редактор событий и действий (EventHandlersPanel);
  • редактор источников данных и моков (DataSourcesPanel);
  • runtime‑превью, исполняющее скомпилированный DSL через @lowcode/runtime-core;
  • подсветку ошибок DSL и expression‑валидации;
  • панель инспектора runtime‑состояния и событий.

Builder-web работает полностью локально в браузере:

  • структура DSL валидируется через @lowcode/dsl;
  • выражения валидируются как по синтаксису, так и по семантике (DSL + AST);
  • React‑код генерируется через @lowcode/dsl-compiler;
  • TSX‑бандл исполняется через общий движок @lowcode/runtime-core;
  • Canvas и превью синхронно отражают состояние одного и того же DSL;
  • для DataSource в превью используются только моки (mock‑конфигурация в DSL), реальные HTTP‑запросы из builder-web не выполняются.

Основные зависимости:

  • @lowcode/dsl — структура DSL, валидация, ExpressionValue, DataSource + DataSource.mock;
  • @lowcode/dsl-compiler — AST, expression‑анализ, TSX‑генерация (учитывает список dataSources и их mock‑конфигурацию);
  • @lowcode/runtime-core — исполняющий движок (bundle → modules → RuntimeInstance, в т.ч. default executor с поддержкой моков);
  • @lowcode/ui-kit — UI‑компоненты редактора (панели, карточки, LayoutShell и т.п.).

Исторически превью использовало @babel/standalone напрямую, но в текущей архитектуре вся ответственность за компиляцию и исполнение бандла вынесена в @lowcode/runtime-core, а builder-web концентрируется на UX и настройке DSL (включая моки).


2. Структура каталога builder-web

apps/builder-web/src/
├─ api/ # in-memory API, mock backend (пока не завязан на реальный сервер)
├─ components/ # Canvas, PreviewPanel, LayoutShell, панели редактора
├─ expressions/ # validateExpression, локальная syntax-валидация
├─ state/ # EditorState, централизованная логика DSL
├─ data/ # шаблоны DSL
├─ pages/ # страницы SPA (home/editor/...)
├─ styles/ # глобальные стили, Tailwind-кастомизация
├─ types.ts # общие типы для редактора
├─ App.tsx # корневой компонент SPA
└─ main.tsx # точка входа Vite/React

2.1. state/ — EditorState

Здесь живёт центральное состояние редактора — EditorState:

  • текущий app: AppSchema | null;
  • activePageId: string | null — активная страница;
  • selectedNodeId: string | null — выбранный компонент в Canvas;
  • issueNodeIds: string[] — ID компонент с ошибками (DSL/выражения/runtime);
  • состояние панелей (раскрытые секции, выбранные вкладки и т.п.);
  • вспомогательные флаги/поля (загрузка, сохранение, источники шаблонов и т.п.).

Также в state/ находятся:

  • хук useEditorState() (контекст редактора);
  • pure‑функции для работы с DSL‑деревом (поиск, обновление, вставка/удаление);
  • хелперы для работы с app.dataSources и mock‑конфигурацией (создание/удаление/обновление источников и их моков).

2.2. components/

Основные визуальные блоки:

  • LayoutShell.tsx — каркас редактора (верхний бар, левая колонка с Canvas, правая с панелями, нижнее превью);
  • Canvas.tsx — дерево компонентов DSL (визуализация ComponentNode);
  • PreviewPanel.tsx — превью на базе @lowcode/runtime-core;
  • PropertiesPanel.tsx — PropertiesPanel v2 (динамически строится по ComponentDefinition);
  • StatePanel.tsx — редактор appState/page.state;
  • EventHandlersPanel.tsx — редактор событий и Actions;
  • DataSourcesPanel.tsx — редактор источников данных и mock‑конфигурации;
  • RuntimeStateInspector.tsx — панель просмотра текущего RuntimeSnapshot;
  • вспомогательные компоненты (toolbar’ы, списки, формы ввода и т.п.).

2.3. expressions/

Слой локальной syntax-валидации выражений в стиле:

validateExpressionSyntax(expression: string): {
isValid: boolean;
errors: string[];
};

Используется для:

  • проверки полей ввода выражений в PropertiesPanel/StatePanel;
  • быстрой подсветки синтаксических ошибок до запуска dsl-compiler;
  • дополнения диагностик, которые приходят из validateExpressionsInAstApp.

2.4. api/

In‑memory API‑клиент (пока без реального сервера):

  • listTemplates() — список доступных шаблонов;
  • createProjectFromTemplate() — создание проекта из шаблона;
  • getCurrentProject() — загрузка текущего DSL;
  • saveProject() — псевдо‑сохранение.

Этот слой будет заменён на настоящий backend‑API по мере развития платформы.


3. EditorState — центральная модель данных

EditorState — ядро builder-web. Он объединяет:

  • DSL‑модель (app: AppSchema);
  • текущий UI‑контекст (активная страница, выбранный компонент);
  • список проблемных компонент (issueNodeIds);
  • настройки редактора (например, какой режим отображения включён);
  • конфигурацию источников данных и моков (app.dataSources + DataSource.mock);
  • точки интеграции с превью (через PreviewPanel).

3.1. DSL‑дерево как источник истины

DSL полностью хранится в памяти в виде AppSchema:

interface AppSchema {
id: string;
name: string;
version: 1;
pages: PageSchema[];
appState?: AppStateSchema;
dataSources?: DataSource[];
eventHandlers?: EventHandler[];
// ...
}

Любое изменение в интерфейсе (поменяли текст кнопки, добавили состояние, настроили обработчик события или mock для DataSource) приводит к иммутабельному изменению AppSchema:

  1. создаётся новая структура AppSchema с применённым патчем;
  2. запись производится в EditorState;
  3. Canvas, панели и PreviewPanel получают новый app и перерисовываются.

3.2. Иммутабельные обновления

Обновления DSL реализованы как pure‑функции:

setApp((prev) =>
updateComponentNode(prev, nodeId, (node) => ({
...node,
props: {
...node.props,
text: 'New label',
},
})),
);

и аналогично для источников данных:

setApp((prev) =>
updateDataSource(prev, dataSourceId, (ds) => ({
...ds,
mock: {
...ds.mock,
enabled: true,
mode: 'static',
value: { items: demoUsers },
},
})),
);

Это гарантирует:

  • отсутствие неожиданных сайд‑эффектов;
  • корректный re-render в React;
  • прозрачную историю изменений (в будущем можно будет навинтить undo/redo).

3.3. Связь EditorState ↔ панели

  • Canvas читает app + activePageId + issueNodeIds.
  • PropertiesPanel читает app + selectedNodeId и обновляет только конкретный компонент.
  • StatePanel обновляет app.appState и page.state.
  • EventHandlersPanel обновляет app.eventHandlers.
  • DataSourcesPanel обновляет app.dataSources и их поле mock.
  • PreviewPanel читает весь app и activePageId, но не меняет DSL напрямую (все изменения идут только через EditorState).

4. Панели редактора

4.1. Canvas

Canvas визуализирует дерево компонентов DSL (ComponentNode). Основные функции:

  • отображение иерархии rootComponent / layoutComponent для активной страницы;
  • выбор компонента (устанавливает selectedNodeId в EditorState);
  • подсветка ошибочных нод (если node.id находится в issueNodeIds);
  • базовые layout‑индикаторы.

В будущем Canvas станет полноценным drag‑and‑drop редактором, но уже сейчас он чётко связан с DSL и системой ошибок.

4.2. PropertiesPanel v2

PropertiesPanel v2 строится динамически на основе реестра компонентов в @lowcode/dsl:

  • для выбранного ComponentNode берётся его type;

  • через реестр ComponentDefinitionRegistry получаем ComponentDefinition:

    • список пропов,
    • типы (string/number/boolean/any/expression и т.п.),
    • default‑значения,
    • метаданные UI (placeholder, multiline, format…);
  • на основе этого описания формируется форма ввода.

Каждый проп может быть в одном из режимов:

  • StaticValue (string, number, boolean, массивы);
  • ExpressionValue ({ kind: 'expression', expression: string }).

PropertiesPanel:

  • переключает режим (static ↔ expression);
  • приводит строковый ввод к правильному типу (число, boolean);
  • вызывает локальный validateExpressionSyntax для expression‑полей;
  • пишет результат обратно в DSL (в node.props).

4.3. StatePanel

StatePanel работает с двумя уровнями состояния:

  • app.appState.variables — глобальное состояние приложения;
  • page.state.variables — локальное состояние конкретной страницы.

Возможности:

  • создание/удаление переменных;
  • изменение типа (string/number/boolean/any);
  • задание initialValue с учётом типа;
  • флаг required;
  • типобезопасная запись в DSL (корректные JSON‑значения).

Именно отсюда приходят переменные state.*, которые затем доступны в выражениях (ExpressionValue) и попадают в контекст выражений AST.

4.4. EventHandlersPanel

EventHandlersPanel управляет массивом EventHandler[] в DSL:

interface EventHandler {
id: ID;
targetComponentId: ID;
eventName: string;
actions: Action[];
}

Панель позволяет:

  • привязать событие к компоненту (onClick, onChange и т.п.);

  • добавить цепочку Actions:

    • navigate;
    • setState (глобальное/по странице);
    • callDataSource;
    • showToast;
    • log и др.;
  • задавать параметры Actions, в том числе через ExpressionValue.

С точки зрения runtime, эти декларативные Actions превращаются в вызовы actions.* и/или RuntimeCommand, обрабатываемые @lowcode/runtime-core.

4.5. DataSourcesPanel и моки

DataSourcesPanel работает с app.dataSources и их mock‑конфигурацией.

4.5.1. Структура DataSource

Упрощённо, в DSL есть два типа источников данных:

export type DataSource = StaticDataSource | RestDataSource;

и общее поле mock:

interface DataSourceMockConfig {
enabled: boolean;
mode: 'static' | 'sequence' | 'error';
value?: StaticValue; // для mode = 'static'
sequence?: StaticValue[]; // для mode = 'sequence'
errorMessage?: string; // для mode = 'error'
delayMs?: number; // необязательная задержка
}

DataSourcesPanel позволяет:

  • создавать/удалять источники данных;
  • настраивать базовые поля REST/STATIC (URL, метод, тело запроса — WIP);
  • включать/выключать mock.enabled;
  • выбирать режим mock (static / sequence / error);
  • задавать value или sequence, а также delayMs и errorMessage.

4.5.2. Поведение моков в превью

В превью builder-web используется default executor из @lowcode/runtime-core:

  • если mock.enabled === true, поведение определяется mock.mode:

    • static — возвращается mock.value,

      • если value не задан и источник kind="static", используется config.data;
      • если value не задан и источник не static, результатом будет null;
    • sequence — значения из mock.sequence перебираются по кругу по dataSource.id;

    • error — выбрасывается ошибка (с mock.errorMessage или дефолтным текстом).

  • если mock.enabled !== true:

    • для kind="static" возвращается config.data;
    • для kind="rest" и других не‑static-видов в превью выбрасывается явная ошибка о том, что «REST data source X без mock не может быть выполнен в превью; реальный HTTP должен выполняться в runtime-host, а для превью нужно включить mock».

Таким образом:

  • builder-web никогда не делает реальные HTTP‑запросы из превью;
  • все данные для DataSource берутся из mock‑конфигурации или config.data;
  • если REST-источник не замокан, пользователь получает понятное сообщение о необходимости настроить mock.

5. Валидация DSL и выражений

Валидация в builder-web многоуровневая.

5.1. Структурная валидация DSL (validateAppSchema)

На входе PreviewPanel (и при некоторых действиях редактора) DSL прогоняется через validateAppSchema из @lowcode/dsl.

Проверяется:

  • корректность структуры приложения, страниц, компонент;
  • уникальность id;
  • валидность appState и page.state (типы и значения);
  • базовая корректность DataSources и EventHandlers;
  • корректность типов полей mock (например, что sequence — массив, delayMs — число и т.п.).

Ошибка возвращается как список строк, которые в builder-web преобразуются в DslValidationIssue { path, message, severity }.

5.2. Локальная syntax‑валидация выражений (validateExpressionSyntax)

В expressions/validateExpression.ts реализована быстрая проверка синтаксиса для ExpressionValue.expression.

Она используется:

  • при вводе выражения в PropertiesPanel/StatePanel;
  • в PreviewPanel для прохода по DSL (без AST), чтобы собрать первичные diagnostics:
    • неправильные скобки;
    • лишние символы;
    • пустые/обрезанные выражения.

5.3. Семантическая валидация выражений (validateExpressionsInAstApp)

На уровне AST, через @lowcode/dsl-compiler, выполняется более глубокая проверка:

const ast = buildAstFromDsl(app);
const exprDiags = validateExpressionsInAstApp(app, ast);

Здесь выражения уже видят:

  • переменные state.* из глобального/страничного состояния;
  • props.* из ComponentDefinition.props (WIP, пока не привязаны к реальным props);
  • data.* из DataSource (включая те, для которых настроены моки);
  • ожидаемые типы пропов (string/number/boolean и т.п.).

Результат — список ExpressionDiagnostic, где у каждого указаны:

  • pageId;
  • componentId;
  • propName;
  • expression;
  • message;
  • severity.

5.4. Сведение ошибок в issueNodeIds

PreviewPanel объединяет источники ошибок:

  1. DslValidationIssue[] (структурная валидация DSL);
  2. ExpressionDiagnostic[] (выражения по DSL + AST);
  3. сообщения локального validateExpressionSyntax;
  4. сообщения об ошибках компиляции/исполнения превью (включая ошибки из mock/executor).

Для этого используются вспомогательные функции:

  • collectIssueNodeIds(app, issues) — попытка восстановить componentId по path в ошибке валидатора;
  • collectNodeIdsFromExpressionDiagnostics(exprDiags) — берёт componentId напрямую из diagnostics;
  • collectNodeIdsFromErrorMessage(app, message) — эвристический поиск нод по текстовому сообщению (по подстрокам node=...).

Итоговый набор ID передаётся в EditorState через onErrorNodeIdsChange([...ids]) и используется Canvas для подсветки нод с ошибками.


6. Превью и интеграция с runtime-core

6.1. Общий pipeline превью

Текущая версия PreviewPanel (components/PreviewPanel.tsx) работает поверх @lowcode/runtime-core.

Высокоуровневая схема:

graph TD
DSL[AppSchema] --> VALIDATE[validateAppSchema<br/>+ локальная проверка выражений]
VALIDATE --> AST[buildAstFromDsl<br/>AstApp]
AST --> EXPR[validateExpressionsInAstApp]
EXPR -->|diagnostics → issueNodeIds| CANVAS[Canvas]
AST --> REACTBNDL[compileDslToReact<br/>(TSX bundle)]
REACTBNDL --> RUNTIME[@lowcode/runtime-core<br/>createRuntimeInstance]
RUNTIME --> PREVIEW[PreviewPanel<br/>RuntimeInstance.RootComponent]

Ключевые шаги внутри PreviewPanel:

  1. Сброс состояния при изменении app:

    • сбрасываются предыдущие ошибки и diagnostics;
    • очищается лог runtime‑событий.
  2. Валидация DSL и выражений:

    • validateDslLocally(app) (обёртка над validateAppSchema);
    • validateExpressionsInDslAppLocally(app) (syntax‑проверка ExpressionValue);
    • buildAstFromDsl(app) + validateExpressionsInAstApp(app, ast) (семантика).
  3. Если есть ошибки — Canvas подсвечивает ноды, превью не компилируется (пользователь сначала исправляет DSL/выражения).

  4. Если ошибок нет:

    • compileDslToReact(app, { language: 'tsx' }) → TSX‑бандл (GeneratedBundleLike);

    • рассчитывается начальное состояние для runtime:

      • buildInitialAppState(app) из app.appState.variables;
      • buildInitialPageState(app, activePageId) из page.state.variables;
      • определяется effectivePageId (текущая или первая страница);
    • собирается initialRuntimeState: Partial<RuntimeSnapshot>.

  5. Передача DataSources и executor’а с моками в runtime-core:

    const dataSources = app.dataSources ?? [];

    const runtimeInstance = createRuntimeInstance({
    bundle,
    components: devPreviewComponents,
    actions: {}, // dev-actions могут быть добавлены поверх
    initialState: initialRuntimeState,
    dataSources,
    dataSourceExecutor: createDefaultDataSourceExecutor(),
    onError: (err) => handleRuntimeError(err),
    });

    Здесь createDefaultDataSourceExecutor() — стандартный executor из @lowcode/runtime-core, который умеет работать с DataSource.mock и никогда не выполняет реальные HTTP‑запросы в превью.

  6. Формирование RootComponent и обёртки:

    const Root = runtimeInstance.RootComponent;

    const Wrapped = (appProps: any) => (
    <Root
    {...appProps}
    initialAppState={initialAppState}
    initialPageState={initialPageState}
    activePageId={effectivePageId}
    onNavigate={(nextPageId) => onNavigate?.(nextPageId)}
    onRuntimeCommand={(command) => runtimeInstance.dispatch(command)}
    >
    <RuntimeStateInspector />
    </Root>
    );

    Wrapped сохраняется в состоянии GeneratedApp и рендерится внутри "белого экрана" превью.

  7. Кнопка Reset runtime в тулбаре:

    • вызывает runtimeInstance.dispatch({ type: 'reset' });
    • увеличивает previewRuntimeKey, чтобы пересоздать React‑дерево превью.

6.2. RuntimeStateInspector и портал

Компонент RuntimeStateInspector живёт внутри дерева RootComponent, но рендерит себя в отдельный контейнер через портал (DOM‑элемент #runtime-inspector-root в панели под превью).

Это позволяет:

  • иметь доступ к RuntimeReactContext внутри runtime‑дерева;
  • при этом показывать состояние в отдельной карточке Runtime state под превью;
  • визуально отделять "экран приложения" и "панель отладки".

6.3. Лог событий runtime (Runtime events)

Через утилиту subscribeToRuntimeEvents из @lowcode/runtime-core PreviewPanel подписывается на изменения рантайма:

unsubscribeRuntimeEvents = subscribeToRuntimeEvents(runtimeInstance, {
listener: (snapshot, reason) => {
setRuntimeEvents((prev) => [{ snapshot, reason, timestamp: Date.now() }, ...prev].slice(0, 50));
},
});
  • RuntimeSnapshot фиксируется для будущей визуализации (пока хранится как any, но в будущем может быть использован для продвинутого инспектора);
  • RuntimeChangeReason форматируется функцией formatRuntimeChangeReason(reason) в человеко‑читаемую строку на английском:
    • Runtime initialized;
    • State updated: scope=global, path=filters.limit;
    • Data source updated: id="users";
    • Navigation: null → page-2;
    • Custom change: tag="debug", meta=....

Карточка Runtime events отображает этот лог и позволяет увидеть, какие события происходили в превью (навигация, изменение state, обновление источников данных и т.п.).

При ошибках выполнения callDataSource (например, mock.mode = "error" или отсутствие mock для REST‑источника) соответствующие события также попадают в лог и подсвечиваются в UI.


7. Подсветка ошибок в Canvas

Canvas подсвечивает компоненты, у которых есть ошибки, при помощи issueNodeIds.

Источник issueNodeIds — PreviewPanel:

  1. После структурной валидации DSL (validateAppSchema) и семантической проверки выражений (validateExpressionsInAstApp) формируется список DslValidationIssue + ExpressionDiagnostic.

  2. Эти структуры преобразуются в набор componentId:

    • через путь path (pages[0].rootComponent.children[1]...);
    • через diag.componentId;
    • через эвристику по тексту ошибок (по подстрокам node=...).
  3. При ошибке компиляции/исполнения превью текст исключения также обрабатывается, чтобы попытаться восстановить ID ноды.

  4. Итоговый массив ID передаётся наверх через onErrorNodeIdsChange(ids) и сохраняется в EditorState.

Canvas, получая issueNodeIds, визуально помечает такие ноды (рамка, цвет, иконка ошибки). В дальнейшем по наведению/клику можно подсвечивать конкретный проблемный проп в PropertiesPanel или привязанный к нему DataSource.


8. SPA‑архитектура и страницы

Редактор — Single Page Application на React.

Типичный набор маршрутов:

/             # стартовая страница / список проектов или шаблонов
/editor # основной режим редактора
/editor/:id # будущая привязка к конкретному проекту

SPA даёт:

  • мгновенные переходы между режимами без перезагрузки страницы;
  • единый EditorState на всём жизненном цикле приложения;
  • возможность держать в DOM одновременно Canvas, Preview, PropertiesPanel, DataSourcesPanel и devtools.

Vite обслуживает один index.html, всё остальное делает React Router.


9. Принципы проектирования builder-web

  1. DSL — единственный источник истины.

    • любой UI‑элемент отражает и редактирует DSL, а не какие-то промежуточные структуры;
    • превью всегда строится из актуального AppSchema.
  2. Иммутабельность и прозрачные обновления.

    • все изменения делаются через pure‑функции обновления DSL;
    • EditorState остаётся предсказуемым, возможен будущий undo/redo.
  3. Разделение слоёв: DSL → AST → runtime.

    • DSL‑валидация и AST‑анализ живут в @lowcode/dsl и @lowcode/dsl-compiler;
    • исполнение бандла, runtime‑state и executor источников данных живут в @lowcode/runtime-core;
    • builder-web отвечает только за UX вокруг этих слоёв.
  4. Ошибки должны быть видимы.

    • любая ошибка (DSL, выражения, runtime, моки) подсвечивается в UI;
    • Canvas и панели позволяют быстро найти и исправить источник.
  5. ExpressionValue — полноценный гражданин DSL.

    • выражения валидируются и по синтаксису, и по семантике;
    • в будущем будут поддерживаться подсветка и интерактивная отладка выражений.
  6. UI-kit как единая дизайн‑система.

    • LayoutShell, панели, карточки, кнопки и формы построены на @lowcode/ui-kit;
    • runtime-host будет использовать тот же UI‑kit, обеспечивая единое ощущение от платформы.
  7. Моки — обязательный слой между превью и внешним миром.

    • builder-web никогда не делает «боевые» запросы к API;
    • превью опирается только на mock‑данные или статический config.data;
    • runtime-host отвечает за реальные HTTP/DB/WebSocket‑вызовы.
  8. Расширяемость и эволюция.

    • любые новые функции проходят тот же путь: DSL → AST → Preview → Runtime;
    • архитектура допускает появление новых компонентов, действий, dataSources, mock‑режимов и выражений без ломки ядра.

10. Далее по плану

Следующие шаги развития builder-web вокруг существующей архитектуры:

  • углубление интеграции с @lowcode/runtime-core (подключение dev‑actions вместо пустого actions: {});

  • дальнейшее развитие системы моков DataSources:

    • улучшенный UI для редактирования mock‑конфигураций;
    • предпросмотр mock‑ответов и "replay" последовательностей;
    • presets/mock‑templates для типовых API (users, list, detail…);
  • развитие Canvas до полноценного drag‑and‑drop редактора;

  • улучшение RuntimeStateInspector (дерево состояния, фильтры по dataSources, diff‑режим);

  • интеграция expression‑подсветки и автодополнения в полях ввода выражений;

  • связь builder-web с внешним API (apps/api) для реального сохранения/загрузки проектов и предварительной компиляции.

Builder-web уже сейчас выступает как центральный интерфейс Lowcode‑платформы, а по мере развития runtime-host и backend‑слоя станет основным рабочим местом для создания и сопровождения DSL‑приложений.