Lowcode Runtime Host SPA
@lowcode/runtime-host (директория apps/runtime-host) — это тонкий хост для выполнения DSL‑приложений в «боевом» режиме. Он построен поверх @lowcode/runtime-core и предназначен для двух сценариев:
- предпродовый просмотр опубликованной версии проекта;
- будущее прод‑окружение, в котором реальный сайт пользователя живёт и работает.
В отличие от builder-web:
- здесь ничего нельзя редактировать — только запускать уже собранное приложение;
- нет Canvas, PropertiesPanel и прочих редакторских панелей;
- используются реальные источники данных (HTTP), а не только моки;
- есть компактная dev‑панель (inspector), которая показывает
RuntimeSnapshotи последнийRuntimeChangeReason.
Runtime-host почти не содержит бизнес‑логики — он:
- получает DSL/схему приложения из backend API;
- компилирует её в бандл с помощью
@lowcode/dsl-compiler; - строит начальный
RuntimeSnapshotиз DSL; - создаёт
RuntimeInstanceчерез@lowcode/runtime-core; - монтирует
RootComponentв React‑дерево; - подключает production‑executor источников данных (HTTP‑запросы) вместо dev‑моков;
- рисует минимальную оболочку (layout + панель инспектора + переключатель темы).
Авторизация
Поскольку backend API теперь мульти‑клиентный, runtime-host обязан ходить в него с тем же JWT, что и builder-web. При открытии окна runtime-host:
- builder-web сериализует текущую auth-сессию (
lowcode:auth) и добавляет её в query‑параметрauth(window.open(...?projectId=...&version=...&auth=<base64>)). В payload попадает весьAuthSessionи информация о режиме хранения (localилиsession). - runtime-host при старте парсит параметр
auth, сохраняет сессию в своёмlocalStorage/sessionStorage(в зависимости от режима) и удаляет параметр из адресной строки. - Все дальнейшие HTTP-запросы выполняются с заголовком
Authorization: Bearer <accessToken>. - При получении
401выполняется тихий refresh (POST /auth/refresh), токены обновляются, и страница продолжает работу.
Если параметр auth отсутствует или refresh не удался, runtime-host получает 401 и сообщает пользователю об ошибке.
Поэтому важно открывать runtime-host только из авторизованного builder-web — дополнительные логины не требуются.
1. Структура приложения
apps/runtime-host/
├─ src/
│ ├─ api/
│ │ └─ client.ts # HTTP‑клиент для получения AppSchema с backend’а
│ ├─ components/
│ │ ├─ inspector/
│ │ │ └─ RuntimeInspectorPanel.tsx # Правая dev‑панель: RuntimeSnapshot + last event
│ │ └─ layout/
│ │ └─ RuntimeHostLayout.tsx # Общий layout: превью слева, inspector справа
│ ├─ hooks/
│ │ ├─ useExternalRuntimeSnapshot.ts # Подписка на RuntimeInstance извне React‑дерева рантайма
│ │ └─ useProjectRuntime.ts # Главный хук инициализации рантайма по projectId/version
│ ├─ runtime/
│ │ ├─ buildInitialSnapshotFromDsl.ts# Построение начального RuntimeSnapshot из AppSchema
│ │ ├─ compileProject.ts # Компиляция AppSchema → GeneratedBundleLike через dsl-compiler
│ │ ├─ httpDataSourceExecutor.ts # Production‑executor DataSource: реальные HTTP‑запросы
│ │ ├─ createTestRuntime.ts # (dev) хелпер для локального тестового runtime
│ │ └─ testBundle.ts # (dev) тестовый TSX‑бандл для отладки без backend’а
│ ├─ App.tsx # Корневой React‑компонент SPA (хедер + превью)
│ ├─ main.tsx # Точка входа Vite (ReactDOM.createRoot / <ThemeProvider/>)
│ ├─ global.d.ts # Глобальные типы: env‑переменные, window.__runtimeHost*
│ └─ global.css / index.css (план) # Базовые стили, в том числе CSS‑переменные темы
├─ typedoc.json # Конфигурация генерации reference‑доков
├─ tsconfig.json # Общий tsconfig для приложения
├─ tsconfig.docs.json # Спец‑tsconfig для TypeDoc
├─ tailwind.config.cjs # Tailwind‑конфиг (общий с ui-kit)
└─ README.md # Локальный overview модуля
Основная логика живёт в четырёх файлах:
src/App.tsx— корневой компонент приложения;src/hooks/useProjectRuntime.ts— хук инициализации рантайма;src/runtime/httpDataSourceExecutor.ts— production‑executor источников данных;src/components/inspector/RuntimeInspectorPanel.tsx— dev‑панель для чтенияRuntimeSnapshot.
2. Жизненный цикл runtime-host
Высокоуровневая последовательность действий при загрузке страницы runtime-host:
1) Прочитать projectId / versionNumber из env (VITE_RUNTIME_HOST_*)
2) Получить AppSchema по HTTP из backend API
3) Скомпилировать AppSchema → TSX/JSX‑бандл (GeneratedBundleLike)
4) Построить начальный RuntimeSnapshot из DSL (учитывая URL и params)
5) Создать RuntimeInstance через createRuntimeInstance(...)
6) Смонтировать runtimeInstance.RootComponent в React‑дерево
7) Подключить dev‑инспектор и подписки на RuntimeSnapshot
2.1. Чтение цели: RuntimeTarget
В App.tsx цель выполнения описывается типом:
export interface RuntimeTarget {
projectId: string | null;
versionNumber: string | null;
}
Значения берутся из Vite‑переменных окружения:
VITE_RUNTIME_HOST_PROJECT_ID;VITE_RUNTIME_HOST_VERSION_NUMBER.
Дополнительно для проксирования dataSources через runtime-backend:
VITE_RUNTIME_BACKEND_URL— базовый URL runtime-backend;VITE_RUNTIME_BACKEND_TOKEN— service token для заголовкаAuthorization: Bearer ....
Это позволяет запускать runtime-host как:
- предпросмотр конкретной версии проекта;
- потенциально — как общий хост, который получает параметры из URL / router.
2.2. Хук useProjectRuntime
Хук useProjectRuntime(target) инкапсулирует весь пайплайн и возвращает:
export type RuntimeStatus =
| 'idle'
| 'loadingDsl'
| 'compiling'
| 'creatingRuntime'
| 'ready'
| 'error';
interface UseProjectRuntimeState {
status: RuntimeStatus;
error: string | null;
runtime: RuntimeInstance | null;
}
Внутри он:
-
По
projectIdиversionNumberвызываетfetchAppSchemaByProjectVersion(HTTP‑клиент вapi/client.ts). -
Передаёт
AppSchemaвcompileAppSchemaToBundle(runtime/compileProject.ts) и получаетGeneratedBundleLike. -
Вызывает
buildInitialSnapshotFromDsl(appSchema)для построенияRuntimeSnapshot:globalStateпоapp.appState.variables;pageStateдля всех страниц;- пустой (или частично инициализированный) блок
dataSources; activePageIdиroute.paramsиз URL (path) с fallback на первую страницу;route.queryизwindow.location.search.
-
Создаёт
RuntimeInstance:const runtime = createRuntimeInstance({
bundle,
components: runtimeHostComponents, // production компоненты с поддержкой attachments
actions: {}, // dev/prod actions будут добавлены позже
initialState: initialSnapshot,
dataSources: appSchema.dataSources ?? [],
dataSourceExecutor: createHttpDataSourceExecutor(),
onError: (error) => {
console.error('[runtime-host] Runtime error:', error);
},
}); -
Экспортирует
bundleиruntimeнаwindowдля отладки:window.__runtimeHostBundle = bundle;
window.__runtimeHostRuntime = runtime; -
Обновляет
statusв зависимости от этапа и ловит ошибки.
2.3. Корневой компонент App
App.tsx отвечает за визуальную оболочку:
- берёт
status,runtime,errorизuseProjectRuntime; - читает тему из
useTheme(ui-kit) и показывает переключательLight / Darkна основеSwitch; - рендерит
RuntimeHostLayout, внутри которого в основную область монтируетсяruntime.RootComponent; - синхронизирует URL с активной страницей,
route.params(подстановка:id) иroute.query(query‑строка); - в хедере отображает:
- имя проекта и номер версии;
- текущий статус;
- текст ошибки, если инициализация не удалась.
Формат route params
Runtime-host поддерживает динамические сегменты пути в стиле :param:
- путь страницы:
/product/:id; - входящий URL:
/product/42; route.paramsв snapshot:{ "id": "42" }.
Формат route query
- URL:
/catalog?q=shoes&page=2; route.queryв snapshot:{ "q": "shoes", "page": "2" }.
route.path хранит исходный шаблон страницы (/product/:id) и используется для обратной сборки URL при navigate.
Если page.path не задан, fallback-путь строится из pageId (/page-id).
Вся логика взаимодействия с рантаймом (dispatch, subscribe) остаётся внутри RuntimeHostLayout и RuntimeInspectorPanel.
3. Production компоненты с поддержкой attachments
Runtime-host использует production версии компонентов Image и Video с поддержкой загрузки attachments.
3.1. Файл src/components/runtimeComponents.tsx
// apps/runtime-host/src/components/runtimeComponents.tsx
import {
defaultDevComponents,
createAttachmentImage,
createAttachmentVideo,
} from '@lowcode/runtime-core';
import type { RuntimeComponentRegistry } from '@lowcode/runtime-core';
import { fetchAttachment } from '../api/client';
/**
* Image компонент с поддержкой загрузки attachments для runtime-host.
*/
const Image = createAttachmentImage({
fetchAttachment,
});
/**
* Video компонент с поддержкой загрузки attachments для runtime-host.
*/
const Video = createAttachmentVideo({
fetchAttachment,
});
/**
* Production компоненты для runtime-host.
*
* Расширяет defaultDevComponents из runtime-core, заменяя
* Image и Video компоненты на версии с поддержкой загрузки attachments.
*/
export const runtimeHostComponents: RuntimeComponentRegistry = {
...defaultDevComponents,
Image,
Video,
};
3.2. Ключевые особенности
- Использование фабрик:
createAttachmentImageиcreateAttachmentVideoиз runtime-core - Общий fetchAttachment: Одна функция загрузки для обоих типов медиа
- Автоматическое кеширование: URL кешируются на 5 минут (TTL из runtime-core)
- Поддержка двух режимов:
sourceType === 'url'→ прямое использование URLsourceType === 'upload'→ загрузка через API с presigned URLs
Важно: внешние URL рендерятся напрямую, без импорта в storage. Это значит, что доступность таких медиа зависит от внешнего хоста.
Подробнее о фабриках attachments см. Runtime-core документацию.
4. Production‑executor источников данных
В отличие от builder‑preview, runtime-host должен работать с реальными данными.
Для этого реализован отдельный executor:
// runtime/httpDataSourceExecutor.ts
export function createHttpDataSourceExecutor(): RuntimeDataSourceExecutor { ... }
Он подключается в useProjectRuntime через опцию dataSourceExecutor и заменяет дефолтный мок‑executor из runtime-core.
3.1. Основные принципы
-
Поддерживаем моки так же, как в preview:
- если в DSL для DataSource включён
mock(mode: static | sequence | error), executor ведёт себя так же, какcreateDefaultDataSourceExecutorизruntime-core; - это позволяет тестировать приложение без реального backend’а.
- если в DSL для DataSource включён
-
Если мока нет — выполняем настоящий HTTP‑запрос:
- используем
baseUrl,pathиmethodизRestDataSource.config; - собираем абсолютный URL и вызываем
fetch/ HTTP‑клиент; - JSON‑результат от backend’а возвращаем в runtime-core, который уже сам обновляет
snapshot.dataSourcesи, при необходимости,pageStateпоassignResultToStateKey. - для
data.<name>используется единый контракт{ value, error, loading, lastResultAt }из runtime-core. - при ошибках/таймаутах executor возвращает понятный текст, без утечек секретов.
- логируем события
dataSource.call.*с маскированными ошибками иdurationMs.
- используем
-
Ошибки не роняют всю SPA:
- HTTP‑ошибки и ошибки парсинга ловятся;
- в
snapshot.dataSources[id]проставляются__lastErrorи__lastErrorAt; - UI приложения может отреагировать на эти поля (например, показать ошибку пользователю).
3.3. Secrets и режимы выполнения
Runtime-host поддерживает два режима работы с SecretValue:
- Proxy‑режим (рекомендуется) — секреты не попадают в браузер. REST‑запросы с
SecretValueвыполняются через backend‑прокси (exported backend / runtime‑backend). - Локальный режим — секреты подставляются в runtime-host из
runtime-config.json/env (подходит только для доверенного окружения).
В режиме preview runtime-host дополнительно запрашивает секреты через API:
GET /projects/:projectId/secrets/runtime.
Формат runtime-config.json:
{
"secrets": {
"API_KEY": "value",
"DB_PASSWORD": "value"
}
}
Источник секретов и приоритет:
- Project Secrets из API (в режиме preview, по
projectId; ключи переопределяют значения локального конфига). VITE_SECRET_*из окружения (напримерVITE_SECRET_API_KEY)runtime-config.json(по умолчанию/runtime-config.json, можно переопределить черезVITE_RUNTIME_CONFIG_URL)
Если секрет не найден, executor возвращает ошибку dataSource с понятным сообщением.
3.2. Связь с runtime-core
httpDataSourceExecutor реализует интерфейс RuntimeDataSourceExecutor из @lowcode/runtime-core и вызывается изнутри applyRuntimeCommand при обработке команды callDataSource.
Таким образом, вся бизнес‑логика работы с источниками данных остаётся в runtime-core, а runtime-host лишь предоставляет конкретную реализацию вызова (HTTP вместо моков).
4. Dev‑панель и инспекция RuntimeSnapshot
Runtime-host содержит встроенную dev‑панель справа — RuntimeInspectorPanel.
4.1. Хук useExternalRuntimeSnapshot
Поскольку панель живёт вне дерева компонентов DSL‑приложения, ей нужен отдельный способ подписаться на изменения RuntimeSnapshot.
Хук useExternalRuntimeSnapshot(runtime: RuntimeInstance | null):
-
подписывается на
runtime.subscribeпри монтировании; -
хранит в локальном состоянии:
- актуальный
snapshot; - последний
RuntimeChangeReason(lastReason);
- актуальный
-
при
unmountотписывается от runtime.
Этот хук переиспользуется и в других потенциальных dev‑панелях.
4.2. RuntimeInspectorPanel
Компонент RuntimeInspectorPanel отображает результаты работы хука:
- в блоке Last event — отформатированное описание
lastReasonчерезformatRuntimeChangeReasonизruntime-core; - в блоке Snapshot — prettified JSON текущего
RuntimeSnapshot.
Оформление панели использует CSS‑переменные shell‑темы:
--rh-bg/--rh-bg-panel/--rh-bg-subtle;--rh-text/--rh-border.
Это гарантирует, что инспектор корректно поддерживает светлую и тёмную тему.
4.3. Layout RuntimeHostLayout
RuntimeHostLayout — обёртка над основным содержимым и панелью инспектора:
┌──────────────────────────────────────────────┬──────────────────────────┐
│ Runtime preview │ Runtime inspector │
│ (RootComponent из runtime-core) │ (snapshot + last event) │
└──────────────────────────────────────────────┴──────────────────────────┘
Слева — произвольное содержимое (обычно RootComponent DSL‑приложения), справа — RuntimeInspectorPanel.
Layout полностью основан на CSS‑переменных темы и не вмешивается в стили приложения пользователя (кроме базовой фоновой области).
5. Темы и интеграция с UI‑kit
Runtime-host использует общую систему темы из @lowcode/ui-kit:
ThemeProviderоборачивает всё приложение вmain.tsx;useThemeдаёт доступ к активной теме (light/dark) и методамsetTheme/toggleTheme;- реальные токены цвета (
lightTheme/darkTheme) живут внутри ui‑кита и используются компонентами (в том числеSwitch).
В App.tsx в хедере отображается переключатель:
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-300">
<span>Light</span>
<Switch checked={isDark} onChange={handleThemeToggle} />
<span>Dark</span>
</div>
Где:
isDarkопределяется поthemeName === 'dark'изuseTheme();- обработчик
handleThemeToggleвызываетsetTheme('dark' | 'light').
Сам ThemeProvider занимается:
- синхронизацией темы с
localStorage(ключlowcode-ui-theme); - установкой
data-theme="light|dark"и классаdarkна корневой контейнер; - предоставлением объекта
ThemeTokens(theme.colors.*) для компонентов ui‑кита.
Runtime-host дополнительно может (опционально) прокидывать значения токенов в CSS‑переменные --rh-* для оформления shell‑оболочки.
6. Экспорт для отладки
Для удобства отладки runtime-host выставляет объекты на window (см. global.d.ts):
interface Window {
__runtimeHostBundle?: GeneratedBundleLike;
__runtimeHostRuntime?: RuntimeInstance;
}
Это позволяет в DevTools:
- изучать структуру скомпилированного бандла (
__runtimeHostBundle.files); - вручную вызывать
__runtimeHostRuntime.dispatch(...)и наблюдать измененияRuntimeSnapshot.
Эти поля предназначены только для dev‑режима и могут быть отключены в будущем production‑сборках.
7. Связь с другими модулями монорепо
Runtime-host лежит на стыке нескольких пакетов:
@lowcode/dsl— описывает структуруAppSchema,PageSchema,DataSourceи т.д.;@lowcode/dsl-compiler— превращает DSL в TSX/JSX‑бандл (GeneratedBundleLike);@lowcode/runtime-core— исполняет бандл, управляет состоянием и источниками данных;@lowcode/ui-kit— предоставляет визуальные компоненты и систему тем.
На практике цепочка выглядит так:
backend API → AppSchema (JSON)
→ runtime-host
→ @lowcode/dsl-compiler (compileAppSchemaToBundle)
→ @lowcode/runtime-core (createRuntimeInstance)
→ React‑корень (RootComponent)
Builder-web, в свою очередь, использует тот же самый runtime-core, но с другим executor’ом источников данных (моки вместо HTTP) и с дополнительными dev‑панелями.
8. Интеграция с builder-web
Runtime-host и builder-web образуют единый цикл разработки и публикации DSL‑приложений.
8.1. Общие артефакты
Оба приложения работают с одними и теми же сущностями:
AppSchemaиз@lowcode/dsl— единый формат описания приложения;@lowcode/dsl-compiler— один и тот же компилятор DSL → TSX/JSX‑бандла;@lowcode/runtime-core— общий рантайм и форматRuntimeSnapshot;@lowcode/ui-kit— одинаковые визуальные компоненты и система тем.
Разница только в том, кто даёт AppSchema и как используется полученный бандл:
- в builder-web DSL формируется и редактируется в браузере, превью использует локальный runtime в том же origin;
- в runtime-host DSL приходит с backend’а по HTTP как опубликованная версия проекта.
8.2. Поток от редактора к runtime-host
Упрощённо цепочка выглядит так:
builder-web (редактор)
→ пользователь собирает DSL и сохраняет проект
→ API сохраняет AppSchema и создаёт новую версию (version_number)
→ по кнопке «Просмотр» / «Открыть в runtime-host»
редактор открывает URL runtime-host
с параметрами ?projectId=...&version=...
→ runtime-host поднимает ту же версию проекта,
что только что была опубликована из builder-web
Таким образом, runtime-host выступает каноничным предпрод‑окружением для версий, созданных в builder-web. Всё, что пользователь увидит в runtime-host, должно максимально совпадать с тем, как приложение будет вести себя на реальном проде.
8.3. Разделение ответственности
-
builder-web отвечает за:
- редактирование DSL‑дерева, state и схем DataSource;
- локальное превью с моками и быстрым циклом итераций;
- управление версиями проекта (draft / publish).
-
runtime-host отвечает за:
- запуск уже опубликованных версий на основе того же DSL;
- работу с реальными HTTP‑источниками данных;
- минимальную оболочку для интеграции с внешней инфраструктурой (домен, роутер, auth и т.д.);
- dev‑инспекцию состояния рантайма без вмешательства в сам editor.
В итоге builder-web и runtime-host используют один и тот же стек (DSL → compiler → runtime-core → ui-kit), но выполняют разные роли: editor vs host.
10. Runtime-backend routing (E6)
Runtime-host проксирует dataSources с SecretValue или db‑типом через runtime-backend:
- REST без секретов → прямой HTTP (как раньше).
- REST с SecretValue → runtime-backend (
/data-sources/execute). - DB dataSources → runtime-backend.
Для этого нужны переменные окружения:
VITE_RUNTIME_BACKEND_URLVITE_RUNTIME_BACKEND_TOKEN
runtime-backend проверяет Authorization: Bearer <token>.
9. План развития runtime-host
Дальнейшие шаги для модуля runtime-host:
-
Продакшн‑actions
- заменить пустой объект
actions: {}на реестр действий, завязанный на реальные сервисы:navigate→ интеграция с SPA‑роутером (history API / React Router);showToast→ продакшн‑тостер поверх ui‑кита;- auth / storage / tracking и т.д.
- заменить пустой объект
-
Роутинг и мульти‑layout страниц
- синхронизация
RuntimeSnapshot.activePageIdс URL + поддержкаpage.pathи:params; - поддержка разных layout’ов для разных страниц приложения.
- синхронизация
-
Расширение dev‑панелей
- лог событий runtime (список последних
RuntimeChangeReason); - отдельная вкладка для
dataSources(статус, payload, ошибка, время последнего вызова); - возможность «перезапускать» отдельные запросы из интерфейса.
- лог событий runtime (список последних
-
Производственная сборка
- режим, в котором бандлы компилируются на стороне API/CI, а runtime-host получает уже готовый JS‑набор;
- отключение dev‑экспортов (
window.__runtimeHost*), подробных логов и лишних проверок; - настройка CSP / sandbox для изоляции кода выражений и сторонних библиотек.
-
Интеграция с системой публикации
- единый endpoint/URL для просмотра опубликованных версий проекта;
- автоматическая подстановка
projectId/versionNumberпри переходе из builder-web.
10. Резюме
Runtime-host SPA — это тонкая оболочка над @lowcode/runtime-core, которая берет на себя:
- загрузку DSL‑схемы проекта из backend’а;
- компиляцию в React‑бандл через
@lowcode/dsl-compiler; - построение начального
RuntimeSnapshot; - создание
RuntimeInstanceс production‑executor’ом источников данных; - рендер RootComponent в минимальный, но аккуратный UI‑shell с поддержкой светлой/тёмной темы;
- отображение dev‑панели состояния (RuntimeSnapshot + last event).
Вся сложная логика исполнения, управления состоянием и работы с источниками данных остаётся в @lowcode/runtime-core. Runtime-host предоставляет готовое окружение, максимально приближенное к тому, как приложение будет вести себя в реальном продакшене.