Архитектура фронтенда
Как Oleafly разделяет настольное приложение между рабочим пространством фронтенда, сервисами на Rust и закреплёнными сайдкарами.
Фронтенд Oleafly — это workspace на pnpm: оболочка приложения находится в
корне репозитория (src/), а переиспользуемый код фронтенда — в packages/*
в виде двенадцати узкоспециализированных пакетов. Бэкенд Tauri находится в
src-tauri/, а переиспользуемый код провайдеров ИИ и агента — в
crates/oleafly-agent/. Общую картину «фронтенд/Rust/сайдкары» см. в
схеме архитектуры в README (откроется в новой вкладке),
а повседневные команды — в разделе Разработка.
Этот документ описывает: что представляет собой каждый пакет, правила инверсии зависимостей, которые держат их чистыми, реестр контрибуций, который подключает функции к оболочке, как работает разрешение модулей и как добавить или извлечь пакет.
Зачем нужен workspace
Заголовок раздела «Зачем нужен workspace»Раньше каждая функция обращалась напрямую к хранилищам Zustand приложения, IPC-клиенту Tauri и UI-киту shadcn. Из-за этого функции нельзя было отделить от оболочки: ничего нельзя было тестировать, переиспользовать или анализировать изолированно. Workspace переворачивает это устройство. Пакеты содержат логику функций и объявляют узкие интерфейсы, называемые портами, для всего, что им нужно. Приложение реализует эти порты поверх своих хранилищ и клиента Tauri и внедряет их. Приложение зависит от пакетов, но никогда наоборот.
Пакеты используются как исходный код TypeScript (их main указывает
на src/index.ts). Нет шага сборки, нет публикации и нет расхождения
версий: Vite, Vitest и tsc разрешают @oleafly/* напрямую в исходники
пакетов через алиасы (см. Разрешение модулей).
| Пакет | Что это | Внедряемые порты | Заметные зависимости |
|---|---|---|---|
@oleafly/latex | Чистая логика LaTeX/рисунков: модель диаграмм, сериализатор TikZ (modelToTikz, двустороннее преобразование встроенной модели), сборщик автономного документа (buildStandaloneDoc), вспомогательные функции для имён и байтов рисунков | нет (чистый) | нет |
@oleafly/ai-core | Отображаемые метаданные провайдеров и выбор моделей на фронтенде, в синхронизации с каталогом на Rust | нет (чистый) | нет |
@oleafly/latex-intelligence | Проверенные типы и доступ к данным корпуса LaTeX, используемого интеллектом редактора | нет (чистый) | нет |
@oleafly/registry | Реестр контрибуций: вкладки рельса, команды палитры/омнибара, наборы инструментов ИИ (подробности ниже) | нет (контрибуции несут собственное поведение) | react (типы) |
@oleafly/preflight | Механизм предпроверки: типизация документа, правила для исходного текста, PDF и библиографии, разбор ATS, подготовка доступного экспорта, подсчёт баллов. pdf-extract поставляется как подпуть @oleafly/preflight/pdf-extract, чтобы pdf.js не попадал в графы node-тестов | нет (чистый) | pdfjs-dist (только подпуть) |
@oleafly/pdf-to-latex | Локальный конвейер восстановления LaTeX из PDF: группировка строк, определение заголовков и колонок, вывод стилей, сопоставление символов, извлечение рисунков, экранирование и сборка документа. Адаптер pdf.js поставляется как подпуть @oleafly/pdf-to-latex/pdf-adapter по той же причине графа node-тестов | нет (чистый) | pdfjs-dist (только подпуть) |
@oleafly/wysiwyg | Ядро ограниченного визуального редактора: общая схема, разбор и сериализация LaTeX и Markdown, а также «сырые» строчные/блочные узлы, которые сохраняют неподдерживаемый исходный текст видимым вместо того, чтобы отбрасывать его | нет (чистый) | нет |
@oleafly/editor | Ядро CodeMirror для LaTeX, Typst и Markdown: CodeMirrorEditor, контроллер представления редактора (insertAtCursor, gotoLine, …), языки и автодополнения, тема, сворачивание кода, закреплённые заголовки, линтеры, latex-mask, предпросмотр формул (KaTeX для LaTeX, Typst для Typst), вспомогательные средства редактирования Typst и преобразование формул из LaTeX в Typst, встроенный визуальный режим для LaTeX и Typst (@oleafly/editor/visual), панель поиска, линтеры орфографии и грамматики | EditorHost (модель документа + настройки, в виде хуков), SpellHost (проверка орфографии/Harper/словарь), setBibKeysProvider, extraExtensions/extraKeymap | @codemirror/*, @replit/codemirror-vim, @replit/codemirror-emacs, katex |
@oleafly/preview | Виртуализированный просмотрщик pdf.js (PdfViewer), контроллер страниц SyncTeX (gotoRect, pageClickToBp), воркер pdf.js и полифиллы WebView (@oleafly/preview/polyfills, импортируется первым в main.tsx) | проп onOpenLink (ссылки в системном браузере), setPdfLogger | pdfjs-dist |
@oleafly/diagram | Визуальный редактор диаграмм для TikZ, Typst (fletcher) и Mermaid: холст React Flow, инспектор фигур, вкладки «Рисование»/«Код», поток «компиляция — предпросмотр — вставка». Для каждого языка в src/languages/ есть писатель, читатель, а также его формы для вставки и файлов, и холст с кодом синхронизируются в обе стороны | DiagramHost (компиляция LaTeX, отрисовка Typst и Mermaid, файловый ввод-вывод, вставка в редактор, исправление с помощью ИИ), контекст DiagramKit (Button/Tooltip/Select/toast/theme) | @xyflow/react, @codemirror/*, @oleafly/latex, @oleafly/editor |
@oleafly/ai-tools | Определения инструментов для проекта, исследования, памяти и рисунков с мутациями, учитывающими подтверждение | AiToolsHost (файлы, компиляция, индекс символов, конвейер рисунков, редактор) | @oleafly/latex |
@oleafly/templates | Галерея шаблонов новых проектов (двухшаговый мастер, категории, предпросмотры, однократные загрузки ресурсов с индикатором прогресса) | TemplatesHost (предпросмотры, загрузка ресурсов, журналирование), TemplatesKit (Button/Tooltip), варианты цветов как пропсы | нет, кроме UI-утилит |
Направление зависимостей, строго соблюдаемое:
Пакет никогда не должен импортировать из src/ (@/…), хранилища Zustand,
@tauri-apps/* или UI-компонентов приложения. Проверка чистоты — часть
гигиены извлечения:
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src # must return nothingПаттерн портов
Заголовок раздела «Паттерн портов»Пакеты объявляют интерфейсы. Приложение реализует их один раз, обычно в виде объекта-адаптера на уровне модуля, замыкающего хранилища. Четыре повторяющихся формы:
Порты-хосты: один interface XHost на пакет, покрывающий нужные ему
сервисы. Приложение собирает адаптер с императивными чтениями хранилищ:
// src/lib/ai-tools.ts (app adapter, abridged)const HOST: AiToolsHost = { getProjectId: () => useFilesStore.getState().projectId, readFileContent, // Tauri client passthrough recompile: () => useCompileStore.getState().recompile(), insertAtCursor, // editor controller // ...};export const createOleaflyTools = (opts?) => createOleaflyToolsCore(HOST, opts);Хранение забот, относящихся только к приложению, внутри адаптера также не пускает зависимости в пакеты. Учётные данные и запросы провайдеров остаются в Rust, поэтому пакеты фронтенда получают только то состояние и те результаты, которые требуются их интерфейсам.
UI-киты: React-контекст (или проп) структурно типизированных
компонентов: Button: ComponentType<{ variant?: …; onClick?: () => void }>.
Приложение передаёт свои компоненты shadcn без изменений, поскольку
меньший структурный тип совместим. Киты также несут toast и хук
useThemeMode().
Порты в виде хуков: для пакетов, которым нужно подписываться на
состояние приложения. EditorHost редактора имеет члены-хуки
(useActivePath(), useSettings()) плюс императивные двойники
(getActivePath()) для слушателей событий. Правила: объект хоста должен
находиться на уровне модуля (стабильная идентичность), а его члены use*
должны вызывать хуки безусловно.
Модульные синглтоны: для сквозного связующего кода;
setPdfLogger(fn), setSpellHost(host), setBibKeysProvider(fn) имеют
пустые умолчания и устанавливаются шимом приложения как побочный эффект
импорта.
Каждое извлечение оставляло тонкий файл по старому пути @/…, который
реэкспортирует (или оборачивает) пакет, поэтому потребители и существующие
пути vi.mock(...) не менялись. Примеры:
src/components/editor/cm/controller.ts (реэкспорт),
src/components/pdf/PdfViewer.tsx (обёртка, внедряющая открыватель ссылок
плагина оболочки), src/lib/ai-tools.ts (адаптер + исходные сигнатуры
фабрики). Новый код может импортировать @oleafly/* напрямую. Шимы
существуют для совместимости, а не как предпочтительный путь.
Реестр контрибуций
Заголовок раздела «Реестр контрибуций»@oleafly/registry — способ подключать функции к оболочке приложения так,
чтобы оболочка о них не знала. Три вида контрибуций:
- Вкладки рельса (
registerRailTab): значок, секция (explore/review/assist, разделители рисуются между непустыми секциями), необязательная видимостьwhen(ctx), необязательный хукuseBadge()(например, количество изменений Git) и компонент боковой панелиpanel. - Команды (
registerCommand): подаются в командную палитру (Cmd+K, группируются поgroup) и/или в омнибар (Cmd+P) черезsurfaces. Метки и значки могут быть функциями контекста (например, «Переключиться на светлую тему»). - Наборы инструментов ИИ (
registerAiToolset): режим чата ("chat","figure"), сопоставленный с фабрикой набора инструментов. Панель чата ищет набор инструментов активного режима вместо жёсткого закрепления одного.
AppContext ({ projectId, projectKind, theme }) строится каждой
отображающей поверхностью из собственных подписок, поэтому условия
when/label остаются реактивными.
Регистрация статическая: src/contributions/ регистрирует все
встроенные элементы (вкладки, команды, наборы инструментов), а main.tsx
вызывает registerContributions() один раз до монтирования оболочки.
Rail, Sidebar, CommandPalette, SearchOmnibar и ChatPanel
отображают только то, что зарегистрировано.
Чтобы добавить вкладку или команду функции, зарегистрируйте её. Правки оболочки не нужны:
// src/contributions/my-feature.tsx (called from src/contributions/index.ts)registerRailTab({ id: "review", label: "Review", icon: MessageSquare, section: "review", order: 55, when: (ctx) => ctx.projectKind !== "image", panel: ReviewPanel,});registerCommand({ id: "omnibar.review", surfaces: ["omnibar"], order: 45, label: "Open review", keywords: "review comments", run: () => useSettingsStore.getState().setRailTab("review"),});Это внутренний API, а не SDK плагинов: формы могут свободно меняться, и ничего не загружается динамически. Отложено до появления реального потребителя: форматы меню экспорта, схемы настроек и поведения в зависимости от типа проекта как контрибуции.
Разрешение модулей
Заголовок раздела «Разрешение модулей»Поскольку пакеты используются как исходный код, один и тот же алиас объявляется в трёх местах, и все три должны оставаться синхронными при добавлении пакета:
tsconfig.json→compilerOptions.paths. Подпути вроде@oleafly/preflight/pdf-extractтребуют собственной явной записи.vite.config.ts→resolve.alias. Строковые алиасы сопоставляются по префиксу, поэтому подпути разрешаются без дополнительной записи.vitest.config.ts→ тот жеresolve.alias, плюсtest.includeдолжен охватыватьpackages/**/*.test.ts. Иначе перенесённые тесты молча выпадают из запуска.
Кроме того: корневой package.json зависит от каждого пакета через
"workspace:*", а в src/styles/globals.css есть
@source "../../packages", чтобы Tailwind v4 сканировал исходники пакетов
в поисках имён классов.
Импорты, работающие только в Vite (?worker&url-воркеры, импорты CSS),
работают из исходников пакетов без изменений, поскольку алиасы указывают
на исходный код. Не помещайте в barrel-файлы, которые импортируют тесты
в окружении node, ничего, что тянет за собой pdf.js: именно поэтому
pdf-extract и просмотрщик пакета preview достижимы только там, где
существует DOM.
Работа с пакетами
Заголовок раздела «Работа с пакетами»- Тесты лежат рядом с исходным кодом (
packages/*/src/*.test.ts) и запускаются в том жеpnpm test, что и тесты приложения. Следите за количеством тестов при перемещении файлов. - Зависимости объявляются свободно (
"react": "*"). pnpm дедуплицирует их относительно корня, где находятся настоящие нижние границы версий. Не объявляйте зависимости, которые пакет не импортирует. - Гейты для любого изменения пакета:
pnpm exec tsc --noEmit,pnpm build,pnpm test(неизменное количество) и приведённая выше проверка чистоты через grep. - Извлечение нового: перемещайте файлы через
git mv(история сохраняется), определите порты, оставьте шимы по старым путям, подключите три алиаса, затем запустите гейты. Не извлекайте функцию, пока активно её дорабатываете. - Лицензирование: каждый пакет —
AGPL-3.0-or-later, как и приложение.
Что остаётся в приложении
Заголовок раздела «Что остаётся в приложении»В src/ остаётся всё, что действительно является приложением фронтенда:
хранилища Zustand, IPC-клиент Tauri (src/lib/tauri.ts), UI-кит shadcn,
компоненты макета и оболочки, адаптеры портов и шимы, src/contributions/
и связующий код функций, по своей природе привязанный к хранилищам
(UI панели предпроверки, плагин встроенного редактирования с ИИ, а также
интеллект кода и наведения, внедряемый в редактор как extraExtensions).
Команды Tauri и интеграция с операционной системой остаются в src-tauri/.
Клиенты провайдеров, потоковая передача и оркестрация агента принадлежат
crates/oleafly-agent/, чтобы секреты и запросы к провайдерам оставались
вне webview.