Skip to main content

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:

  1. builder-web сериализует текущую auth-сессию (lowcode:auth) и добавляет её в query‑параметр auth (window.open(...?projectId=...&version=...&auth=<base64>)). В payload попадает весь AuthSession и информация о режиме хранения (local или session).
  2. runtime-host при старте парсит параметр auth, сохраняет сессию в своём localStorage/sessionStorage (в зависимости от режима) и удаляет параметр из адресной строки.
  3. Все дальнейшие HTTP-запросы выполняются с заголовком Authorization: Bearer <accessToken>.
  4. При получении 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;
}

Внутри он:

  1. По projectId и versionNumber вызывает fetchAppSchemaByProjectVersion (HTTP‑клиент в api/client.ts).

  2. Передаёт AppSchema в compileAppSchemaToBundle (runtime/compileProject.ts) и получает GeneratedBundleLike.

  3. Вызывает buildInitialSnapshotFromDsl(appSchema) для построения RuntimeSnapshot:

    • globalState по app.appState.variables;
    • pageState для всех страниц;
    • пустой (или частично инициализированный) блок dataSources;
    • activePageId и route.params из URL (path) с fallback на первую страницу;
    • route.query из window.location.search.
  4. Создаёт 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);
    },
    });
  5. Экспортирует bundle и runtime на window для отладки:

    window.__runtimeHostBundle = bundle;
    window.__runtimeHostRuntime = runtime;
  6. Обновляет 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. Ключевые особенности

  1. Использование фабрик: createAttachmentImage и createAttachmentVideo из runtime-core
  2. Общий fetchAttachment: Одна функция загрузки для обоих типов медиа
  3. Автоматическое кеширование: URL кешируются на 5 минут (TTL из runtime-core)
  4. Поддержка двух режимов:
    • sourceType === 'url' → прямое использование URL
    • sourceType === '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’а.
  • Если мока нет — выполняем настоящий 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_URL
  • VITE_RUNTIME_BACKEND_TOKEN

runtime-backend проверяет Authorization: Bearer <token>.


9. План развития runtime-host

Дальнейшие шаги для модуля runtime-host:

  1. Продакшн‑actions

    • заменить пустой объект actions: {} на реестр действий, завязанный на реальные сервисы:
      • navigate → интеграция с SPA‑роутером (history API / React Router);
      • showToast → продакшн‑тостер поверх ui‑кита;
      • auth / storage / tracking и т.д.
  2. Роутинг и мульти‑layout страниц

    • синхронизация RuntimeSnapshot.activePageId с URL + поддержка page.path и :params;
    • поддержка разных layout’ов для разных страниц приложения.
  3. Расширение dev‑панелей

    • лог событий runtime (список последних RuntimeChangeReason);
    • отдельная вкладка для dataSources (статус, payload, ошибка, время последнего вызова);
    • возможность «перезапускать» отдельные запросы из интерфейса.
  4. Производственная сборка

    • режим, в котором бандлы компилируются на стороне API/CI, а runtime-host получает уже готовый JS‑набор;
    • отключение dev‑экспортов (window.__runtimeHost*), подробных логов и лишних проверок;
    • настройка CSP / sandbox для изоляции кода выражений и сторонних библиотек.
  5. Интеграция с системой публикации

    • единый 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 предоставляет готовое окружение, максимально приближенное к тому, как приложение будет вести себя в реальном продакшене.