Skip to main content

Архитектура 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, AstExpressionValuedslToAst/astTypes.ts
  • DSL → AST: buildAstFromDsldslToAst/buildAst.ts
  • Контекст выражений: buildExpressionContextdslToAst/expressionContext.ts
  • Валидация выражений: validateExpressionsInAstAppexpression/validateExpressions.ts
  • Генерация React: generateReactAppcodegen/reactGenerator.ts
  • Генерация HTML: generateHtmlAppcodegen/htmlGenerator.ts
  • Типы бандла: GeneratedBundle, GeneratedFilecodegen/types.ts
  • Фасадные функции: compileDslToReact, compileDslToHtmlindex.ts

1. Роль DSL Compiler в платформе

@lowcode/dsl-compiler — ключевой слой Lowcode‑платформы между DSL‑схемой и исполняемым кодом. Он:

  1. принимает DSL‑дерево приложения (AppSchema) из редактора или backend API;
  2. валидирует схему через валидатор в пакете @lowcode/dsl;
  3. собирает нормализованное AST‑дерево (AstApp);
  4. строит контекст и AST выражений, валидирует выражения по типам;
  5. генерирует набор файлов (React/TSX или статический HTML+CSS);
  6. отдаёт результат:
    • backend‑сервису @lowcode/api для последующей сборки/раздачи;
    • приложению @lowcode/builder-web для runtime‑превью и подсветки ошибок;
    • runtime‑core, который уже исполняет React‑бандл и управляет состоянием.

Основные входные/выходные функции:

  • 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 → подставляем его;
  • все «лишние» пропы, не описанные в 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 (camelCasekebab-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

Чтобы добавить новый тип компонента:

  1. Добавить его описание в реестр компонентов @lowcode/dsl (ComponentDefinition).
  2. Обновить UI‑kit и/или runtime‑компоненты, если это необходимо.
  3. Убедиться, что buildAstFromDsl корректно обрабатывает новые пропы (обычно это происходит автоматически через normalizedProps).
  4. При необходимости добавить особую логику в генераторы 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 → ASTdslToAst/buildAst.tsконструктор AST, индексы по id, нормализация пропов
Контекст выраженийdslToAst/expressionContext.tsExpressionContext для state/props/data
Валидация выраженийexpression/validateExpressions.tsпроход по AstApp и проверка выражений по типам
React‑генераторcodegen/reactGenerator.tsJSX/TSX‑генерация
HTML‑генераторcodegen/htmlGenerator.tsстатический HTML + CSS
Типы бандлаcodegen/types.tsGeneratedBundle, GeneratedFile, опции
Публичный APIindex.tsфасады compileDslToReact/Html

9. Выводы

  1. @lowcode/dsl-compiler — центральный слой трансляции DSL в исполняемый код.
  2. Валидация, AST, анализ выражений и генерация кода разделены по слоям, что упрощает сопровождение и расширение.
  3. AST — единая основа для всех таргетов (React, HTML, будущие генераторы) и для статического анализа.
  4. Единый формат бандла (GeneratedBundle) делает интеграцию с backend, builder-web и runtime-core простой и предсказуемой.
  5. Архитектура уже готова к развитию: добавлению новых компонентов, усилению микро‑DSL выражений и расширению набора таргетов генерации кода.