Архитектура DSL Compiler
Как работает трансляция DSL → AST → анализ выражений → React/HTML, какие этапы проходит проект и где находится основная логика компиляции.
🎯 Цель страницы
Эта страница описывает внутреннее устройство пакета @lowcode/dsl-compiler:
- этапы трансляции DSL;
- структуру AST и используемые индексы;
- как устроен анализ и валидация выражений;
- механизм генерации React/TSX и статического HTML;
- как API и builder-web вызывают компилятор;
- как DSL‑compiler сейчас интегрирован с
@lowcode/runtime-core; - какие места уже подготовлены под развитие микро‑DSL для выражений и layout.
Справочник по модулю:
- Общий обзор:
@lowcode/dsl-compiler - AST-типы:
AstApp,AstPage,AstComponent,AstExpressionValue—dslToAst/astTypes.ts - DSL → AST:
buildAstFromDsl—dslToAst/buildAst.ts - Контекст выражений:
buildExpressionContext—dslToAst/expressionContext.ts - Валидация выражений:
validateExpressionsInAstApp—expression/validateExpressions.ts - Генерация React:
generateReactApp—codegen/reactGenerator.ts - Генерация HTML:
generateHtmlApp—codegen/htmlGenerator.ts - Типы бандла:
GeneratedBundle,GeneratedFile—codegen/types.ts - Фасадные функции:
compileDslToReact,compileDslToHtml—index.ts
1. Роль DSL Compiler в платформе
@lowcode/dsl-compiler — ключевой слой Lowcode‑платформы между DSL‑схемой и исполняемым кодом. Он:
- принимает DSL‑дерево приложения (
AppSchema) из редактора или backend API; - валидирует схему через валидатор в пакете
@lowcode/dsl; - собирает нормализованное AST‑дерево (
AstApp); - строит контекст и AST выражений, валидирует выражения по типам;
- генерирует набор файлов (React/TSX или статический HTML+CSS);
- отдаёт результат:
- backend‑сервису
@lowcode/apiдля последующей сборки/раздачи; - приложению
@lowcode/builder-webдля runtime‑превью и подсветки ошибок; - runtime‑core, который уже исполняет React‑бандл и управляет состоянием.
- backend‑сервису
Основные входные/выходные функции:
- DSL → AST:
buildAstFromDsl(app: AppSchema): AstApp - AST + выражения → диагностика:
validateExpressionsInAstApp(app: AppSchema, ast: AstApp): ExpressionDiagnostic[] - AST → React:
generateReactApp(ast: AstApp, options?): GeneratedBundle - AST → HTML:
generateHtmlApp(ast: AstApp, options?): GeneratedBundle - DSL → React (one‑shot):
compileDslToReact(app: AppSchema, options?) - DSL → HTML (one‑shot):
compileDslToHtml(app: AppSchema, options?)
Высокоуровневая схема в контуре всей платформы:
graph TD
DSL[DSL JSON (AppSchema)] --> VALIDATE[validateAppSchema<br/>(@lowcode/dsl)]
VALIDATE --> AST[buildAstFromDsl<br/>AstApp]
AST --> ANALYZE[validateExpressionsInAstApp]
ANALYZE -->|diagnostics| EDITOR[builder-web<br/>Canvas/PropertiesPanel]
AST --> RGEN[generateReactApp]
AST --> HGEN[generateHtmlApp]
RGEN --> RBNDL[React TSX/JSX bundle]
RBNDL --> RUNTIME[@lowcode/runtime-core<br/>compileBundleToJsModules + createRuntimeInstance]
HGEN --> HBNDL[HTML bundle<br/>(.html + styles.css)]
2. Структура пакета
Базовая структура в packages/dsl-compiler/src/:
codegen/ ← генерация React/HTML из AST
htmlGenerator.ts ← AST → статический HTML + базовый CSS
reactGenerator.ts ← AST → React (TSX/JSX)
types.ts ← типы GeneratedBundle, опции генераторов
dslToAst/ ← преобразование DSL → AST + контекст выражений
astTypes.ts ← типы AST (AstApp, AstPage, AstComponent, AstExpressionValue)
buildAst.ts ← сборка AST, индексы по id, normalizedProps
expressionContext.ts ← построение ExpressionContext для state/props/data
expression/ ← анализ и валидация выражений на AST
validateExpressions.ts ← проход по AstApp, проверка типов/идентификаторов
index.ts ← публичный API (compileDslToReact/Html, re‑export)
Ключевые сущности:
- AST‑типизация — описана в
dslToAst/astTypes.ts; - преобразование DSL → AST — в
dslToAst/buildAst.ts; - построение контекста выражений — в
dslToAst/expressionContext.ts; - типовая валидация выражений — в
expression/validateExpressions.ts; - генерация кода — в
codegen/reactGenerator.tsиcodegen/htmlGenerator.ts.
3. Этапы компиляции
Компиляция разделена на четыре последовательных шага.
3.1. Валидация DSL (в пакете @lowcode/dsl)
Первый шаг выполняется не внутри dsl-compiler, а в базовом пакете @lowcode/dsl:
import { validateAppSchema } from '@lowcode/dsl';
const result = validateAppSchema(app);
if (!result.ok) {
// список ошибок в result.errors
}
Проверяются, в частности:
- обязательные поля приложения:
version,id,name,pages; - корректность страниц (
PageSchema):id,name,path,rootComponent; - структура дерева компонентов (
ComponentNode):id,type,props,layout,children; - уникальность всех
id(страниц, компонент, источников данных, обработчиков событий); - корректность значений пропов и layout с учётом
ExpressionValue; - структура и значения блоков состояния:
appState,page.state:- ключи переменных по паттерну
^[a-zA-Z_][a-zA-Z0-9_]*$; - типы
string | number | boolean | any; - совместимость
type/initialValue; - поддержка флага
requiredи диагностика пустыхinitialValue.
- ключи переменных по паттерну
Только после успешной валидации имеет смысл строить AST.
3.2. Построение AST (DSL → AST)
Основная функция:
import { buildAstFromDsl } from '@lowcode/dsl-compiler';
const ast: AstApp = buildAstFromDsl(appSchema, {
// опционально
throwOnValidationError: true,
});
AstApp — нормализованное представление приложения, в котором:
- все страницы представлены как
AstPage[]; - каждое дерево компонентов развёрнуто в
AstComponentс:pageId,parentId,depth;- строковым путём
path(для диагностических сообщений); componentDefinitionиз DSL‑реестра (@lowcode/dsl);- флагом
canHaveChildren; - картой
normalizedProps.
3.2.1. Normalized props и выражения (AstExpressionValue)
При построении AST buildAstFromDsl использует ComponentDefinition из @lowcode/dsl:
-
для каждого описанного пропа:
- если он указан в DSL → берём значение из
node.props; - если не указан, но есть
defaultValue→ подставляем его;
- если он указан в DSL → берём значение из
-
все «лишние» пропы, не описанные в
ComponentDefinition, копируются как есть; -
значения выражений (
ExpressionValue) преобразуются вAstExpressionValue:- парсятся парсером выражений (
@lowcode/expression-parser); - в поле
astкладётся AST выражения (илиnullпри синтаксической ошибке); - вычисляется
inferredType(string/number/boolean/any/unknown); - собираются
usedIdentifiersиusedMemberPaths.
- парсятся парсером выражений (
В итоге normalizedProps — единый источник правды для генераторов и анализаторов, где уже учтены значения по умолчанию и есть структурированная информация о выражениях.
3.2.2. Индексы и связь сущностей
При обходе DSL‑дерева buildAstFromDsl дополнительно формирует индексы:
pagesById: Map<ID, AstPage>— быстрый доступ к страницам;componentsById: Map<ID, AstComponent>— поиск компонента поid;dataSourcesById: Map<ID, DataSource>— источники данных поid;eventHandlersByTargetId: Map<ID, EventHandler[]>— обработчики событий, сгруппированные поtargetComponentId.
Эти индексы позволяют:
- быстро находить обработчики событий для компонента при генерации React‑кода;
- реализовывать интеллектуальную подсветку и переходы в редакторе;
- использовать AST в анализаторах и оптимизациях.
3.2.3. Пример AstComponent
const node: AstComponent = {
id: 'button-cta',
type: 'Button',
normalizedProps: {
label: {
kind: 'expression',
expression: 'state.buttonLabel',
ast: null, // или ExpressionNode, если парсинг успешен
inferredType: 'string',
usedIdentifiers: ['state'],
usedMemberPaths: ['state.buttonLabel'],
},
},
componentDefinition: {
type: 'Button',
canHaveChildren: false,
props: {
label: {
kind: 'string',
name: 'label',
required: true,
defaultValue: 'Button',
},
},
},
canHaveChildren: false,
children: [],
parentId: 'container-1',
depth: 1,
pageId: 'page-home',
path: 'pages[id="page-home"].rootComponent.children[0]',
};
3.3. Контекст и валидация выражений
После построения AstApp можно запустить типовую валидацию выражений:
import { validateExpressionsInAstApp } from '@lowcode/dsl-compiler';
const diagnostics = validateExpressionsInAstApp(appSchema, ast);
Под капотом используется ExpressionContext, описывающий доступные идентификаторы и их типы.
Основные пространства имён:
state.*— переменные изappStateиpage.state;props.*— пропы компонента изComponentDefinition.props;data.*— состояния источников данных (data.<dataSourceId>поAppSchema.dataSources);- групповые пространства имён
state,props,data.
Проверки, выполняемые на этом этапе:
- неизвестные идентификаторы и member‑пути (
foo.bar,state.unknownFieldи т.п.); - соответствие типов выражения ожидаемому типу пропа (
string/number/boolean/any); - базовые ошибки и предупреждения (например, использование неинициализированного
state.*).
Результат — список диагностик:
export interface ExpressionDiagnostic {
pageId: ID;
componentId: ID;
propName: string; // например "props.label" или "layout.width"
expression: string;
message: string;
severity: 'error' | 'warning';
}
Эти данные используются builder-web для подсветки проблемных компонентов и пропов. В превью редактора часть ошибок по data.* сейчас может быть понижена до уровня warning, чтобы не блокировать превью, пока анализатор выражений не научится учитывать все варианты конфигурации dataSources.
3.4. Генерация React/HTML
После получения AstApp запускаются генераторы.
3.4.1. AST → React (TSX/JSX)
Вход:
import { generateReactApp } from '@lowcode/dsl-compiler';
const bundle = generateReactApp(ast, {
language: 'tsx', // или "jsx"
});
Выход: GeneratedBundle с набором файлов, например:
App.tsx— точка входа React‑приложения;Page_<id>.tsx— отдельный компонент для каждой страницы.
Ключевые особенности текущей реализации:
-
App.tsx:- выступает тонкой оболочкой над страницами, не хранит своего локального состояния через
useState; - принимает от внешнего runtime (например,
@lowcode/runtime-core) уже собранные пропсы: глобальное/страничное состояние (state), снапшот источников данных (data) и обработчик событийhandleEvent; - по
activePageId(также приходящему сверху) выбирает нужныйPage_<id>и передаёт емуstate,dataиhandleEventкак пропы; - саму навигацию и управление состоянием не реализует — этим занимается runtime‑слой.
- выступает тонкой оболочкой над страницами, не хранит своего локального состояния через
-
Page_xxx.tsx:- экспортирует компонент вида
Page_<id>(props: PageProps); - тип
PagePropsвключает минимумstate, опциональноdataиhandleEvent, а при необходимости может расширяться; - разворачивает
AstComponentдерева страницы в JSX, используяnormalizedProps; - статические пропы сериализуются напрямую (
text="Hello",count={42}); - выражения передаются как
{/* expression */}изAstExpressionValue.expressionи вычисляются в рантайме через лёгкийevalExpression(в превью builder-web); - layout‑пропы конвертируются либо в обычные JSX‑пропы, либо в inline‑style (по договорённостям);
- события (
onClick,onChangeи т.п.) берутся из DSL‑обработчиков и генерируются как пропы, вызывающиеhandleEvent?.(componentId, eventName, event).
- экспортирует компонент вида
React‑генератор ничего не знает о том, где и как будет выполняться код — этим занимается @lowcode/runtime-core. Задача генератора — получить корректный TSX/JSX, который затем можно отдать в Babel / TypeScript / любой другой toolchain.
3.4.2. AST → HTML/CSS
HTML‑генератор создаёт статический бандл для простого предпросмотра или экспорта.
import { generateHtmlApp } from '@lowcode/dsl-compiler';
const bundle = generateHtmlApp(ast, options);
Выход: GeneratedBundle с файлами:
styles.css— базовые стили;page-<id>.html— один HTML‑файл на страницу.
Особенности:
- layout‑свойства превращаются в inline‑style (
camelCase→kebab-case); - статические пропы сериализуются в
data-prop-*атрибуты; - выражения (
ExpressionValue) сериализуются вdata-expr-*для отладки и потенциального клиентского runtime.
Это делает HTML‑бандл пригодным для статического хостинга и простого предпросмотра без React/runtime-core.
4. AST как единый слой абстракции
AST (AstApp) — единое, типизированное и нормализованное представление приложения. Он нужен, чтобы:
- отвязать генераторы от деталей исходного DSL;
- иметь слой, где подставлены
defaultValueи учтены ограничения компонентов; - хранить служебные поля (
pageId,depth,path), полезные для диагностик и инструментов редактора; - хранить структурированную информацию о выражениях (
AstExpressionValue).
Любой новый таргет (SSR‑код, документация, storybook‑страницы, JSON‑схемы) может опираться на уже готовый AST, не касаясь деталей DSL.
5. GeneratedBundle — единый формат бандлов
Генераторы React и HTML возвращают один и тот же формат:
export interface GeneratedFile {
filename: string;
content: string;
}
export interface GeneratedBundle {
files: GeneratedFile[];
}
Преимущества:
- builder-web может напрямую отдавать
bundle.filesв runtime‑core; - backend (
@lowcode/api) может сохранять файлы на диск, архивировать и деплоить; - внешний tooling (CLI, CI) обрабатывает бандлы единообразно.
6. Взаимодействие с runtime-core и builder-web
6.1. Связка dsl-compiler → runtime-core
В текущей архитектуре превью в builder‑web выглядит так:
AppSchema (из редактора)
↓
validateAppSchema (@lowcode/dsl)
↓
buildAstFromDsl + validateExpressionsInAstApp (@lowcode/dsl-compiler)
↓
compileDslToReact (@lowcode/dsl-compiler)
↓
GeneratedBundle (TSX/JSX)
↓
createRuntimeInstance (@lowcode/runtime-core)
↓
RuntimeInstance.RootComponent (React)
runtime-core принимает GeneratedBundle и уже сам отвечает за:
- компиляцию TSX/JSX в JS (через Babel);
- загрузку модулей (
compileBundleToJsModules,moduleLoader,executeBundle); - управление
RuntimeSnapshotи командойdispatch; - devtools‑интеграцию (подписки, события, инспектор).
С точки зрения dsl-compiler важно только то, что:
- структура файлов в бандле предсказуема (входной
App.tsxплюсPage_*.tsx); - React‑код использует только то, что runtime‑core умеет инжектировать (компоненты, actions, пропсы‑инициализаторы и
handleEvent).
6.2. Использование в builder-web
Builder‑web использует компилятор в двух режимах.
Аналитический слой (AST + diagnostics)
- берёт текущий
AppSchemaиз редактора; - вызывает
buildAstFromDslиvalidateExpressionsInAstApp; - раскладывает
ExpressionDiagnosticпоcomponentId; - подсвечивает проблемные компоненты и пропы в Canvas и PropertiesPanel;
- использует диагностические пути
path/pageIdдля навигации.
Runtime‑превью (compiled preview)
- передаёт текущий
AppSchemaвcompileDslToReactи получаетGeneratedBundle; - отдаёт бандл в
@lowcode/runtime-core.createRuntimeInstance; - runtime‑core компилирует и исполняет бандл, даёт
RootComponent; - превью монтирует
RootComponentвPreviewPanelи прокидывает начальное состояние,onNavigateи пр.
В результате в редакторе показывается тот же React‑код, который потом будет исполняться в runtime‑host SPA, а не какая‑то отдельная «фейковая» реализация.
7. Расширение компилятора
Архитектура dsl-compiler специально упрощает расширения.
7.1. Новые компоненты DSL
Чтобы добавить новый тип компонента:
- Добавить его описание в реестр компонентов
@lowcode/dsl(ComponentDefinition). - Обновить UI‑kit и/или runtime‑компоненты, если это необходимо.
- Убедиться, что
buildAstFromDslкорректно обрабатывает новые пропы (обычно это происходит автоматически черезnormalizedProps). - При необходимости добавить особую логику в генераторы React/HTML (например, специальные layout‑преобразования).
7.2. Выражения (micro‑DSL)
Выражения в DSL задаются как:
interface ExpressionValue {
kind: 'expression';
expression: string; // микро-DSL на основе ограниченного JS
}
За работу с ними отвечают два слоя:
@lowcode/expression-parser— парсит строку в AST выражения, собирает идентификаторы и member‑пути, выводит примитивный тип;@lowcode/dsl-compiler— строитExpressionContextи проверяет, что выражения используют допустимые идентификаторы и соответствуют ожидаемым типам пропов.
В будущем сюда добавятся:
- более строгая грамматика (ограничение допустимых конструкций);
- запрет опасных операций (доступ к
window,evalи т.п.); - precompute простых выражений на этапе компиляции.
7.3. Layout и дизайн‑система
Текущая реализация layout минималистична (частично inline‑style, частично обычные props). Дальнейшие шаги:
- генерация CSS‑классов на основе layout‑свойств;
- привязка к дизайн‑системе и токенам темы из
@lowcode/ui-kit; - отдельные таргеты под SSR/CSR и статический экспорт.
8. Быстрый справочник по файлам
| Компонент | Путь | Описание |
|---|---|---|
| AST типы | dslToAst/astTypes.ts | структуры AstApp, AstPage, AstComponent, AstExpressionValue |
| DSL → AST | dslToAst/buildAst.ts | конструктор AST, индексы по id, нормализация пропов |
| Контекст выражений | dslToAst/expressionContext.ts | ExpressionContext для state/props/data |
| Валидация выражений | expression/validateExpressions.ts | проход по AstApp и проверка выражений по типам |
| React‑генератор | codegen/reactGenerator.ts | JSX/TSX‑генерация |
| HTML‑генератор | codegen/htmlGenerator.ts | статический HTML + CSS |
| Типы бандла | codegen/types.ts | GeneratedBundle, GeneratedFile, опции |
| Публичный API | index.ts | фасады compileDslToReact/Html |
9. Выводы
@lowcode/dsl-compiler— центральный слой трансляции DSL в исполняемый код.- Валидация, AST, анализ выражений и генерация кода разделены по слоям, что упрощает сопровождение и расширение.
- AST — единая основа для всех таргетов (React, HTML, будущие генераторы) и для статического анализа.
- Единый формат бандла (
GeneratedBundle) делает интеграцию с backend, builder-web и runtime-core простой и предсказуемой. - Архитектура уже готова к развитию: добавлению новых компонентов, усилению микро‑DSL выражений и расширению набора таргетов генерации кода.