Архитектура 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:
- создаётся новая структура
AppSchemaс применённым патчем; - запись производится в EditorState;
- 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 объединяет источники ошибок:
DslValidationIssue[](структурная валидация DSL);ExpressionDiagnostic[](выражения по DSL + AST);- сообщения локального
validateExpressionSyntax; - сообщения об ошибках компиляции/исполнения превью (включая ошибки из 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:
-
Сброс состояния при изменении
app:- сбрасываются предыдущие ошибки и diagnostics;
- очищается лог runtime‑событий.
-
Валидация DSL и выражений:
validateDslLocally(app)(обёртка надvalidateAppSchema);validateExpressionsInDslAppLocally(app)(syntax‑проверкаExpressionValue);buildAstFromDsl(app)+validateExpressionsInAstApp(app, ast)(семантика).
-
Если есть ошибки — Canvas подсвечивает ноды, превью не компилируется (пользователь сначала исправляет DSL/выражения).
-
Если ошибок нет:
-
compileDslToReact(app, { language: 'tsx' })→ TSX‑бандл (GeneratedBundleLike); -
рассчитывается начальное состояние для runtime:
buildInitialAppState(app)изapp.appState.variables;buildInitialPageState(app, activePageId)изpage.state.variables;- определяется
effectivePageId(текущая или первая страница);
-
собирается
initialRuntimeState: Partial<RuntimeSnapshot>.
-
-
Передача
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‑запросы в превью. -
Формирование
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и рендерится внутри "белого экрана" превью. -
Кнопка
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:
-
После структурной валидации DSL (
validateAppSchema) и семантической проверки выражений (validateExpressionsInAstApp) формируется списокDslValidationIssue+ExpressionDiagnostic. -
Эти структуры преобразуются в набор componentId:
- через путь
path(pages[0].rootComponent.children[1]...); - через
diag.componentId; - через эвристику по тексту ошибок (по подстрокам
node=...).
- через путь
-
При ошибке компиляции/исполнения превью текст исключения также обрабатывается, чтобы попытаться восстановить ID ноды.
-
Итоговый массив 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
-
DSL — единственный источник истины.
- любой UI‑элемент отражает и редактирует DSL, а не какие-то промежуточные структуры;
- превью всегда строится из актуального
AppSchema.
-
Иммутабельность и прозрачные обновления.
- все изменения делаются через pure‑функции обновления DSL;
- EditorState остаётся предсказуемым, возможен будущий undo/redo.
-
Разделение слоёв: DSL → AST → runtime.
- DSL‑валидация и AST‑анализ живут в
@lowcode/dslи@lowcode/dsl-compiler; - исполнение бандла, runtime‑state и executor источников данных живут в
@lowcode/runtime-core; - builder-web отвечает только за UX вокруг этих слоёв.
- DSL‑валидация и AST‑анализ живут в
-
Ошибки должны быть видимы.
- любая ошибка (DSL, выражения, runtime, моки) подсвечивается в UI;
- Canvas и панели позволяют быстро найти и исправить источник.
-
ExpressionValue — полноценный гражданин DSL.
- выражения валидируются и по синтаксису, и по семантике;
- в будущем будут поддерживаться подсветка и интерактивная отладка выражений.
-
UI-kit как единая дизайн‑система.
- LayoutShell, панели, карточки, кнопки и формы построены на
@lowcode/ui-kit; - runtime-host будет использовать тот же UI‑kit, обеспечивая единое ощущение от платформы.
- LayoutShell, панели, карточки, кнопки и формы построены на
-
Моки — обязательный слой между превью и внешним миром.
- builder-web никогда не делает «боевые» запросы к API;
- превью опирается только на mock‑данные или статический
config.data; - runtime-host отвечает за реальные HTTP/DB/WebSocket‑вызовы.
-
Расширяемость и эволюция.
- любые новые функции проходят тот же путь: 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‑приложений.