@lowcode/runtime-core
@lowcode/runtime-core — это движок выполнения сгенерированных приложений, общий для:
- превью DSL-приложений в builder-web;
- будущего отдельного runtime-host SPA;
- любых сред, где нужно исполнить React-бандл, сгенерированный
@lowcode/dsl-compiler.
Runtime-core почти ничего не знает про конкретный UI, но знает всё про:
- структуру рантайм-состояния (
RuntimeSnapshot), - типы команд (
RuntimeCommand), - протокол причин изменений (
RuntimeChangeReason), - контракт инстанса (
RuntimeInstance), - контекст компонентов и действий (
RuntimeContext), - работу с источниками данных (
RuntimeDataSourceExecutor+ snapshot.dataSources), - базовые devtools-хелперы (форматирование причин изменений, фильтрация событий, React-хуки).
Этот README описывает:
- архитектуру runtime-core;
- структуру файлов и модулей;
- ключевые сущности (snapshot, команды, actions, components, devtools, dataSources);
- как runtime-core используется в builder-web;
- как он будет использоваться в отдельном runtime-host SPA;
- план развития runtime-core и runtime-host.
1. Назначение runtime-core
Runtime-core обеспечивает общий исполнительный слой для DSL-приложений:
- принимает на вход сгенерированный бандл (TSX/JSX/TS/JS-файлы);
- компилирует его в JS-модули и исполняет в изолированном окружении;
- предоставляет реестр визуальных компонентов (React-компоненты);
- предоставляет реестр действий (actions) для императивных операций;
- ведёт runtime-состояние приложения (глобальный state, состояние страниц, источники данных);
- принимает и обрабатывает типизированные команды (
RuntimeCommand); - уведомляет хост (builder-web / runtime-host SPA) о всех изменениях через
RuntimeChangeReasonи devtools-подписки; - делегирует фактическое выполнение источников данных во внешний executor (
RuntimeDataSourceExecutor).
Главная цель: один рантайм для всех окружений, чтобы DSL-приложение вело себя одинаково в превью и в настоящем runtime-host.
2. Структура пакета
packages/runtime-core/
├─ src/
│ ├─ index.ts # Публичный API: типы, createRuntimeInstance, хуки, devtools
│ ├─ core/
│ │ ├─ compileBundle.ts # TSX/JSX/TS/JS → CommonJS JS-модули (через @babel/standalone)
│ │ ├─ executeBundle.ts # Выполнение JS-модулей и поиск App-компонента
│ │ └─ moduleLoader.ts # In-memory загрузчик модулей (require + module.exports)
│ ├─ react/
│ │ ├─ RuntimeRoot.tsx # React-обёртка над App-компонентом + RuntimeReactContext
│ │ ├─ createReactRuntimeInstance.ts # Реализация createRuntimeInstance для React
│ │ └─ runtimeHooks.ts # Набор React-хуков для snapshot/state/dataSources
│ ├─ state/
│ │ └─ runtimeState.ts # Внутреннее состояние рантайма, snapshot + listeners + команды
│ ├─ runtime/
│ │ ├─ defaultDataSourceExecutor.ts # Дефолтный executor источников данных с поддержкой моков
│ ├─ types/
│ │ └─ runtimeDataSourceExecutor.ts # Публичный контракт для executor’а источников данных
│ ├─ devtools/
│ │ └─ runtimeDevtools.ts # Devtools-хелперы: форматтер, type-guards, subscribeToRuntimeEvents
│ └─ components/ # (опционально) dev-компоненты для превью
├─ dist/
├─ package.json
└─ README.md # Текущий файл
Структура может немного меняться по мере развития, но основные слои остаются теми же: core (компиляция/исполнение), state/runtime (состояние и команды), react (обёртка и хуки), runtime (executor источников данных), devtools (инструменты для инспектора и логов).
3. Ключевые сущности
3.1. RuntimeSnapshot
RuntimeSnapshot — это «снимок» текущего состояния рантайма.
Он включает:
export interface RuntimeSnapshot {
globalState: Record<string, unknown>;
pageState: Record<string, Record<string, unknown>>;
activePageId: string | null;
dataSources: Record<string, unknown>;
}
globalState— глобальное состояние приложения (аналогappStateв DSL);pageState— состояние по страницам (pageId → state);activePageId— текущая активная страница (илиnull);dataSources— состояние источников данных.
Состояние dataSources хранится по ключу dataSourceId и может включать:
- произвольные поля результата (
items,user,totalи т.п.); - служебные meta-поля, которые заполняет runtime-core:
__lastCallAt: number— последний момент вызоваcallDataSource;__lastRefetchAt: number— последний моментrefetchDataSource;__lastPayload: unknown— последний payload;__lastResultAt: number— последний успешный результат executor’а;__lastErrorAt: number— последний момент ошибки;__lastError?: string— текст последней ошибки.
Модель может быть уточнена в будущих версиях (например, явные статусы loading/success/error).
3.2. RuntimeCommand
Команда, которую внешний код может отправить в рантайм:
type RuntimeCommand =
| { type: 'navigate'; pageId: string }
| { type: 'setState'; scope: 'global' | 'page'; pageId?; path: string[]; value: unknown }
| { type: 'refetchDataSource'; dataSourceId: string }
| { type: 'callDataSource'; dataSourceId: string; payload?: unknown }
| { type: 'reset' }
| { type: 'custom'; name: string; payload?: unknown };
Команды обрабатываются на уровне state/runtimeState.ts (функция applyRuntimeCommand), которая:
- формирует новый snapshot;
- записывает изменение в нужную область (
globalState,pageState,dataSources,activePageId); - вызывает
updateRuntimeSnapshot, который уведомляет всех подписчиков.
3.3. RuntimeChangeReason
Причина, по которой изменился snapshot. Используется для devtools и внешних подписчиков:
init— первичная инициализация;stateChanged— изменение глобального или страничного состояния;dataSourceCallStarted— запрос на получение данных из источника;dataSourceChanged— изменения, связанные с источниками данных;navigation— навигация между страницами;custom— произвольное пользовательское событие.
Devtools-утилиты (formatRuntimeChangeReason, type-guards) работают поверх этого типа.
3.4. RuntimeContext
Контекст выполнения рантайма:
export interface RuntimeContext {
components: RuntimeComponentsRegistry;
actions: RuntimeActionsRegistry;
}
components— реестр React-компонентов (имя → компонент);actions— реестр действий (имя → функция).
Этот контекст инжектируется в sandbox при выполнении скомпилированного бандла.
3.5. RuntimeInstance
Экземпляр рантайма — то, что получает хост (builder-web, runtime-host SPA):
export interface RuntimeInstance {
RootComponent: ComponentType;
getSnapshot(): RuntimeSnapshot;
subscribe(listener: (snapshot: RuntimeSnapshot, reason: RuntimeChangeReason) => void): () => void;
dispatch(command: RuntimeCommand): void;
}
Создаётся через createRuntimeInstance(options).
3.6. React-хуки
Пакет экспортирует набор React-хуков для работы с рантаймом:
useRuntimeSnapshot()— подписка на целыйRuntimeSnapshot;useRuntimeDispatch()— доступ кdispatchиз React-дерева;useGlobalState(path?: string[])— чтение/подписка на частьglobalState;usePageState(path?: string[], pageId?: string)— чтение/подписка на state страницы;usePageStateObject(pageId?: string)— получение всего state страницы;useDataSource(id: string)— чтение состояния конкретного DataSource поid.
Они работают поверх общего контекста RuntimeReactContext, который создаётся в RuntimeRoot.
3.7. Devtools-хелперы
В devtools/runtimeDevtools.ts находятся утилиты:
-
formatRuntimeChangeReason(reason)— возвращает англоязычную строку с описанием причины изменения (для логов/UI); -
type-guards:
isNavigationChange(reason);isStateChangedReason(reason);isDataSourceChangedReason(reason);isCustomChangeReason(reason);
-
subscribeToRuntimeEvents(runtime, options)— обёртка надruntime.subscribeс фильтрацией по видам событий и произвольномуpredicate.
4. Пайплайн исполнения
Полный путь от DSL до живого React-приложения выглядит так:
DSL → @lowcode/dsl-compiler → TSX-бандл
→ @lowcode/runtime-core.compileBundleToJsModules → JS CommonJS-модули
→ @lowcode/runtime-core.executeBundle → AppComponent
→ @lowcode/runtime-core.createRuntimeInstance → RuntimeInstance.RootComponent
4.1. compileBundleToJsModules
- Принимает
GeneratedBundleLike— набор файловfilename + content(TSX/JSX/TS/JS); - компилирует их через
@babel/standaloneс пресетамиreact,typescriptи плагиномtransform-modules-commonjs; - возвращает in-memory карту CommonJS-модулей.
4.2. moduleLoader
- Эмулирует
require+module.exports; - разрешает только относительные импорты;
- мапит импорт
reactна реальный React; - делает доступными:
- все компоненты из
RuntimeContext.componentsкак top-level имена; - объект
actionsизRuntimeContext.actionsкак top-level переменнуюactions.
- все компоненты из
4.3. executeBundle
- Создаёт loader над скомпилированными модулями;
- загружает модуль
App.tsx(по соглашению); - ищет экспорт
Appилиdefault; - возвращает
AppComponentилиnullпри ошибке.
4.4. createRuntimeInstance
-
компилирует бандл в JS-модули;
-
исполняет бандл и получает
AppComponent; -
создаёт
InternalRuntimeState:snapshot+initialSnapshot;listenersдля подписок;dataSourcesRegistryиdataSourceExecutor(если переданы);
-
формирует
RuntimeContext(components + actions); -
создаёт
RootComponent, который оборачиваетAppComponentвRuntimeRootи пробрасывает в него пропсы хоста; -
возвращает
RuntimeInstanceсRootComponent,getSnapshot,subscribe,dispatch.
5. Источники данных и RuntimeDataSourceExecutor
5.1. Интерфейс executor’а
Runtime-core не знает, как выполнять HTTP/DB/WebSocket — он лишь делегирует выполнение во внешний executor.
Контракт:
export interface RuntimeDataSourceExecutor {
execute(options: {
snapshot: RuntimeSnapshot;
action: CallDataSourceAction;
dataSource: DataSource;
}): Promise<unknown>;
}
Где:
snapshot— актуальное состояние на момент вызова (можно использоватьactivePageId, state и т.п.);action— DSL-экшнcallDataSource(восстановлен из команды);dataSource— описание источника данных из DSL (kind,config,mock).
5.2. Поведение applyRuntimeCommand для DataSources
Команда callDataSource:
-
Обновляет
snapshot.dataSources[dataSourceId]:- проставляет
__lastCallAtи__lastPayload;
- проставляет
-
Генерирует
RuntimeChangeReasonсkind: 'dataSourceChanged'; -
Если указан
dataSourceExecutorи он знает этотdataSourceId:- вызывает
executor.execute({ snapshot: nextSnapshot, action, dataSource }); - по
Promise:- при успехе объединяет результат с meta-полями и обновляет
snapshot.dataSources; - при ошибке проставляет
__lastErrorAtи__lastError;
- при успехе объединяет результат с meta-полями и обновляет
- вызывает
-
Если у
CallDataSourceActionестьassignResultToStateKeyи естьactivePageId, результат записи:- раскладывается по пути, либо пишется под ключ
assignResultToStateKeyвpageState[activePageId].
- раскладывается по пути, либо пишется под ключ
Команда refetchDataSource:
- обновляет
__lastRefetchAtдля нужногоdataSourceId; - генерирует
RuntimeChangeReasonсkind: 'dataSourceChanged'.
5.3. createDefaultDataSourceExecutor
В runtime/defaultDataSourceExecutor.ts находится базовая реализация executor’а, ориентированная на preview в builder-web.
Особенности:
-
static dataSource без мока → всегда возвращает
config.data; -
rest / другие виды без
mock.enabled === true→ бросают осмысленную ошибку вида:REST data source "UsersAPI" (id=users-ds) has no mock and cannot be executed in preview. In production runtime-host is expected to perform HTTP request: ... To use this data source in builder preview, configure a mock in the Data Sources panel.
-
поддерживает
mockв DSL (DataSource.mock):-
mode: 'static'- если
mock.valueзадан → возвращаетmock.value; - если нет, то:
- для
kind: 'static'→ используетсяconfig.data; - для остальных →
null;
- для
- если
-
mode: 'sequence'sequence: any[]— цикл значений поdataSourceId;- хранит счётчик по каждому источнику;
- пустая
sequence→ возвращаетnull;
-
mode: 'error'- если есть
mock.errorMessage→ кидает её; - иначе — дефолтное сообщение
REST data source "<name>" (id=<id>) mock error (mode="error").;
- если есть
-
delayMs- если указан, executor делает искусственную задержку ответа через
setTimeout(симуляция сети).
- если указан, executor делает искусственную задержку ответа через
-
Важный момент: createDefaultDataSourceExecutor не выполняет реальных HTTP-запросов. Для production runtime-host ожидается отдельный executor, который будет реализовывать настоящие REST-вызовы.
6. Devtools и подписки на события
runtime-core предоставляет API для devtools:
-
formatRuntimeChangeReason(reason)— человеко-читаемое англоязычное описание причины изменения snapshot’а; -
type-guards по видам причин:
isNavigationChange(reason);isStateChangedReason(reason);isDataSourceChangedReason(reason);isCustomChangeReason(reason);
-
subscribeToRuntimeEvents(runtime, options)— обёртка надruntime.subscribe, которая позволяет:- слушать только интересующие
kinds; - дополнительно фильтровать события через
predicate; - получать (snapshot, reason) только для нужных событий.
- слушать только интересующие
Пример:
const unsubscribe = subscribeToRuntimeEvents(runtimeInstance, {
kinds: ['navigation', 'dataSourceChanged'],
predicate: (reason) => reason.kind === 'dataSourceChanged' && reason.dataSourceId === 'ds-users',
listener: (snapshot, reason) => {
console.log(formatRuntimeChangeReason(reason));
},
});
В связке с React-хуками (useRuntimeSnapshot, useDataSource, и т.д.) этого достаточно, чтобы собрать собственную панель devtools: лог событий, инспектор состояния, просмотр dataSources.
7. Использование в builder-web (preview)
В превью builder-web (PreviewPanel) runtime-core используется так:
-
Валидация DSL (структура + выражения).
-
Компиляция DSL → TSX-бандл через
@lowcode/dsl-compiler. -
Построение начального состояния:
initialAppStateизapp.appState;initialPageStateдля активной страницы изpage.state;initialDataSourcesState(по умолчанию пустые записи или прединициализация).
-
Создание runtime-инстанса:
const runtimeInstance = createRuntimeInstance({
bundle,
components: runtimeComponents,
actions: runtimeActions,
initialState: {
globalState: initialAppState,
pageState: activePageId ? { [activePageId]: initialPageState } : {},
activePageId,
dataSources: initialDataSourcesState,
},
dataSources: app.dataSources ?? [],
dataSourceExecutor: createDefaultDataSourceExecutor(),
onError: (err) => {
console.error('[PreviewPanel] Runtime error:', err);
},
}); -
Монтаж
runtimeInstance.RootComponentв панель превью. -
Использование:
useRuntimeSnapshotи других хуков внутри дерева превью (например, вRuntimeStateInspector);subscribeToRuntimeEventsсformatRuntimeChangeReasonдля панели Runtime events.
Так превью ведёт себя как минимальный runtime-host, полностью основанный на @lowcode/runtime-core.
8. Использование в runtime-host SPA (план)
Отдельное приложение runtime-host SPA будет тонкой обёрткой над runtime-core.
Предполагаемый сценарий:
-
Получить от backend’а DSL или уже скомпилированный бандл.
-
При необходимости — прогнать DSL через
@lowcode/dsl-compiler(на сервере или на клиенте). -
Собрать реестр компонентов на базе
@lowcode/ui-kit:import * as UI from '@lowcode/ui-kit';
const runtimeComponents = {
Container: UI.Panel,
Text: UI.Text,
Button: UI.Button,
Input: UI.Input,
// ... остальные DSL-компоненты
}; -
Собрать реестр actions, завязанный на реальные сервисы:
navigate— через SPA-роутер;callDataSource/refetchDataSource— через HTTP-клиент (axios/fetch), обновляющийdataSources;showToast— через продакшн UI-тостер;- работа с auth/storage и т.п.
-
Создать
RuntimeInstanceчерезcreateRuntimeInstanceи смонтироватьRootComponentв корень SPA. -
По желанию — подключить devtools-панель поверх runtime-core (state/events/dataSources).
Вся тяжёлая логика исполнения (команды, snapshot, dataSources, devtools) уже реализована в runtime-core и переиспользуется между builder-web и runtime-host.
9. Тестирование
Текущая структура тестов для runtime-core:
-
core-слой — тесты компиляции и загрузки:
compileBundleToJsModules(обработка TSX/JSX/TS/JS);moduleLoader(относительные импорты, кеширование, виртуальныйreact);executeBundle(поискApp/default, обработка ошибок).
-
state/runtime-слой — тесты работы со snapshot’ом:
- инициализация начального состояния по
initialState; - обработка команд (
navigate,setState,callDataSource,refetchDataSource,reset); - поведение при ошибках executor’а (запись
__lastError).
- инициализация начального состояния по
-
DataSources — тесты
defaultDataSourceExecutor:- static dataSource без моков →
config.data; - rest без моков → осмысленная ошибка с подсказкой про runtime-host и моки;
mock.mode = 'static' | 'sequence' | 'error'+delayMs;- корректное поведение при
mock.enabled === false.
- static dataSource без моков →
-
React-оболочка — тесты
createReactRuntimeInstanceиRuntimeRoot:- корректный проброс пропсов в
AppComponent; - наличие
RuntimeReactContextв дереве; - интеграция
dispatchи подписок.
- корректный проброс пропсов в
-
devtools — тесты
runtimeDevtools:formatRuntimeChangeReasonдля всех стандартныхkind;- корректность type-guards;
- фильтрация событий в
subscribeToRuntimeEvents.
Запуск тестов (через Jest в монорепо):
pnpm --filter @lowcode/runtime-core test
10. Резюме
@lowcode/runtime-core — это единый движок выполнения DSL-приложений, который:
- принимает сгенерированный TSX/JSX-бандл от
@lowcode/dsl-compiler; - компилирует и исполняет его в контролируемом окружении;
- управляет runtime-состоянием (global/page/dataSources/activePageId);
- обрабатывает типизированные команды (
navigate,setState,callDataSource,refetchDataSource,reset,custom); - делегирует работу с источниками данных в
RuntimeDataSourceExecutor(с дефолтной реализацией через моки); - предоставляет удобный контракт для хоста (
RuntimeInstance); - даёт devtools-хелперы и хуки для инспекции состояния и событий;
- переиспользуется в превью builder-web и в будущем runtime-host SPA.
Всё, что связано с UI, backend’ом и конкретным окружением, живёт снаружи runtime-core. Здесь находятся только общие абстракции и алгоритмы, обеспечивающие предсказуемое поведение DSL-приложений во всех средах выполнения.