Архітектура фронтенду
Як у Oleafly настільний застосунок поділено між фронтенд-робочим простором, сервісами Rust і зафіксованими sidecar-бінарниками.
Фронтенд Oleafly — це робочий простір pnpm: оболонка застосунку живе в корені репозиторію (src/), а багаторазовий фронтенд-код — у packages/* у вигляді дванадцяти вузькоспрямованих пакетів. Бекенд Tauri живе в src-tauri/, а багаторазовий код провайдерів ШІ та агентів — у crates/oleafly-agent/. Див. схему архітектури в README (відкривається в новій вкладці) для загальної картини фронтенд/Rust/sidecar і сторінку Розробка щодо щоденних команд.
Цей документ охоплює: що собою являє кожен пакет, правила інверсії залежностей, які зберігають їх чистими, реєстр внесків, що під’єднує можливості до оболонки, як працює розв’язання модулів, а також як додати або винести пакет.
Навіщо робочий простір
Розділ «Навіщо робочий простір»Колись кожна можливість напряму дотягувалася до сторів Zustand застосунку, до клієнта Tauri IPC і до UI-кітів shadcn. Це робило можливості невіддільними від оболонки: нічого не можна було тестувати, повторно використовувати чи осмислювати окремо. Робочий простір інвертує цю ситуацію. Пакети тримають логіку можливостей і оголошують вузькі інтерфейси, названі портами, для всього, що їм потрібно. Застосунок реалізує ці порти поверх своїх сторів і клієнта 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 | Локальний конвеєр відтворення PDF у LaTeX: групування рядків, виявлення заголовків і колонок, визначення стилю, відображення символів, витягання рисунків, екранування та збирання документа. Адаптер pdf.js постачається як підшлях @oleafly/pdf-to-latex/pdf-adapter з тієї самої причини щодо тестових графів node | немає (чистий) | pdfjs-dist (лише підшлях) |
@oleafly/wysiwyg | Ядро обмеженого візуального редактора: спільна схема, розбір/серіалізація LaTeX і Markdown, а також сирі вузли inline/block, які зберігають непідтримуване джерело видимим замість того, щоб його відкидати | немає (чистий) | немає |
@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/тема) | @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()) для слухачів подій. Правила: об’єкт host повинен бути на рівні модуля (стабільна тотожність), а його члени use* мають викликати хуки безумовно.
Модульні синглтони: для наскрізного зв’язування setPdfLogger(fn), setSpellHost(host), setBibKeysProvider(fn) мають стандартні реалізації no-op і встановлюються шимом застосунку як побічний ефект імпорту.
Шими
Розділ «Шими»Кожне винесення лишало тонкий файл за старим шляхом @/…, який реекспортує (або обгортає) пакет, тож споживачі та наявні шляхи vi.mock(...) ніколи не зазнавали змін. Приклади: src/components/editor/cm/controller.ts (реекспорт), src/components/pdf/PdfViewer.tsx (обгортка, що впроваджує відкривач посилань shell-плагіна), 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), працюють без змін із вихідних кодів пакетів, бо аліаси вказують на джерело. Тримайте все, що тягне за собою pdf.js, поза барелами, які імпортують тести в середовищі node: саме тому 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, клієнт Tauri IPC (src/lib/tauri.ts), UI-кіт shadcn, компоненти макета/оболонки, адаптери портів і шими, src/contributions/, а також зв’язувальний код можливостей, який за своєю природою прив’язаний до сторів (UI панелі передперевірки, плагін вбудованого редагування ШІ та інтелектуальні функції для коду й наведення, впроваджені в редактор як extraExtensions). Команди Tauri та інтеграція з операційною системою лишаються в src-tauri/. Клієнти провайдерів, потокова передача та оркестрація агентів належать до crates/oleafly-agent/, щоб секрети й запити до провайдерів залишалися поза webview.