Architecture du frontend
Comment Oleafly répartit l'application de bureau entre le workspace frontend, les services Rust et les sidecars épinglés.
Le frontend d’Oleafly est un workspace pnpm : le shell de l’application vit à la racine du dépôt (src/), et le code frontend réutilisable vit dans packages/* sous forme de douze packages ciblés. Le backend Tauri vit dans src-tauri/, tandis que le code réutilisable pour les fournisseurs d’IA et l’agent vit dans crates/oleafly-agent/. Consultez le schéma d’architecture du README (s’ouvre dans un nouvel onglet) pour la répartition frontend/Rust/sidecar et Développement pour les commandes du quotidien.
Ce document couvre : ce qu’est chaque package, les règles d’inversion des dépendances qui les gardent purs, le registre de contributions qui câble les fonctionnalités dans le shell, le fonctionnement de la résolution des modules, et comment ajouter ou extraire un package.
Pourquoi un workspace
Section intitulée « Pourquoi un workspace »Chaque fonctionnalité accédait autrefois directement aux stores Zustand de l’application, au client IPC Tauri et à la bibliothèque UI shadcn. Les fonctionnalités étaient ainsi indissociables du shell : rien ne pouvait être testé, réutilisé ou analysé isolément. Le workspace inverse cette logique. Les packages portent la logique des fonctionnalités et déclarent des interfaces étroites, appelées ports, pour tout ce dont ils ont besoin. L’application implémente ces ports au-dessus de ses stores et de son client Tauri, puis les injecte. L’application dépend des packages, jamais l’inverse.
Les packages sont consommés comme code source TypeScript (leur main pointe vers src/index.ts). Il n’y a ni étape de build, ni publication, ni décalage de version : Vite, Vitest et tsc résolvent tous @oleafly/* directement vers les sources des packages via des alias (voir Résolution des modules).
Les packages
Section intitulée « Les packages »| Package | Ce que c’est | Ports injectés | Dépendances notables |
|---|---|---|---|
@oleafly/latex | Logique LaTeX/figures pure : le modèle de diagrammes, le sérialiseur TikZ (modelToTikz, aller-retour du modèle embarqué), le constructeur de documents autonomes (buildStandaloneDoc), les utilitaires de noms et d’octets des figures | aucun (pur) | aucune |
@oleafly/ai-core | Métadonnées d’affichage des fournisseurs et sélection des modèles côté frontend, maintenues en parité avec le catalogue Rust | aucun (pur) | aucune |
@oleafly/latex-intelligence | Types validés et accès aux données pour le corpus LaTeX utilisé par l’intelligence de l’éditeur | aucun (pur) | aucune |
@oleafly/registry | Le registre de contributions : onglets du rail, commandes de la palette et de l’omnibar, ensembles d’outils IA (détails ci-dessous) | aucun (les contributions portent leur propre comportement) | react (types) |
@oleafly/preflight | Le moteur de prévérification : typage des documents, règles source/PDF/références, analyse ATS, préparation des exports accessibles, notation. pdf-extract est livré via le sous-chemin @oleafly/preflight/pdf-extract pour que pdf.js reste hors des graphes de tests node | aucun (pur) | pdfjs-dist (sous-chemin uniquement) |
@oleafly/pdf-to-latex | Le pipeline local de reconstruction de PDF vers LaTeX : regroupement des lignes, détection des titres et des colonnes, inférence de style, mise en correspondance des symboles, extraction des figures, échappement et assemblage du document. L’adaptateur pdf.js est livré via le sous-chemin @oleafly/pdf-to-latex/pdf-adapter pour la même raison de graphe de tests node | aucun (pur) | pdfjs-dist (sous-chemin uniquement) |
@oleafly/wysiwyg | Le cœur de l’éditeur visuel contraint : schéma partagé, analyse et sérialisation LaTeX et Markdown, et nœuds bruts inline/bloc qui gardent la source non prise en charge visible au lieu de la supprimer | aucun (pur) | aucune |
@oleafly/editor | Le cœur CodeMirror pour LaTeX, Typst et Markdown : CodeMirrorEditor, le contrôleur de vue de l’éditeur (insertAtCursor, gotoLine, …), langages + complétions, thème, pliage, titres épinglés, linters, latex-mask, aperçu mathématique (KaTeX pour LaTeX, Typst pour Typst), aides d’édition Typst et conversion des maths LaTeX vers Typst, mode Visuel intégré pour LaTeX et Typst (@oleafly/editor/visual), panneau de recherche, linters d’orthographe et de grammaire | EditorHost (modèle de document + réglages, en forme de hooks), SpellHost (correcteur orthographique/Harper/dictionnaire), setBibKeysProvider, extraExtensions/extraKeymap | @codemirror/*, @replit/codemirror-vim, @replit/codemirror-emacs, katex |
@oleafly/preview | La visionneuse pdf.js virtualisée (PdfViewer), le contrôleur de pages SyncTeX (gotoRect, pageClickToBp), le worker pdf.js et les polyfills WebView (@oleafly/preview/polyfills, importés en premier dans main.tsx) | prop onOpenLink (liens vers le navigateur système), setPdfLogger | pdfjs-dist |
@oleafly/diagram | Le compositeur visuel de diagrammes pour TikZ, Typst (fletcher) et Mermaid : canevas React Flow, inspecteur de formes, onglets Dessin/Code, flux compiler, prévisualiser puis insérer. Chaque langage de src/languages/ possède un writer, un reader ainsi que ses formes d’insertion et de fichier, et le canevas et le code restent synchronisés dans les deux sens | DiagramHost (compilation LaTeX, rendu Typst et Mermaid, E/S de fichiers, insertion dans l’éditeur, correction IA), contexte DiagramKit (Button/Tooltip/Select/toast/theme) | @xyflow/react, @codemirror/*, @oleafly/latex, @oleafly/editor |
@oleafly/ai-tools | Définitions des outils de projet, de recherche, de mémoire et de figures, avec des mutations conscientes des approbations | AiToolsHost (fichiers, compilation, index des symboles, pipeline de figures, éditeur) | @oleafly/latex |
@oleafly/templates | La galerie de modèles de nouveaux projets (assistant en deux étapes, catégories, prévisualisations, téléchargements uniques des ressources avec progression) | TemplatesHost (prévisualisations, téléchargements des ressources, journalisation), TemplatesKit (Button/Tooltip), options de couleur en props | rien au-delà des utilitaires UI |
Sens des dépendances, strictement appliqué :
Un package ne doit jamais importer depuis src/ (@/…), un store Zustand, @tauri-apps/* ou les composants UI de l’application. La vérification de pureté fait partie de l’hygiène d’extraction :
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src # must return nothingLe motif des ports
Section intitulée « Le motif des ports »Les packages déclarent des interfaces. L’application les implémente une fois, généralement sous forme d’un objet adaptateur au niveau du module qui capture ses stores. Quatre formes récurrentes :
Ports d’hôte : une interface XHost par package couvrant les services dont il a besoin. L’application construit l’adaptateur avec des lectures impératives des stores :
// 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);Regrouper ce qui est propre à l’application dans l’adaptateur maintient aussi les dépendances hors des packages. Les identifiants et les requêtes des fournisseurs restent en Rust, donc les packages frontend ne reçoivent que l’état et les résultats qu’exigent leurs interfaces.
Kits UI : un contexte React (ou une prop) de composants typés structurellement : Button: ComponentType<{ variant?: …; onClick?: () => void }>. L’application transmet ses composants shadcn tels quels, car le type structurel plus petit est compatible. Les kits portent aussi toast et un hook useThemeMode().
Ports en forme de hooks : pour les packages qui doivent s’abonner à l’état de l’application. L’EditorHost de l’éditeur possède des membres en forme de hooks (useActivePath(), useSettings()) ainsi que des jumeaux impératifs (getActivePath()) pour les écouteurs d’événements. Règles : l’objet hôte doit être au niveau du module (identité stable) et ses membres use* doivent appeler des hooks inconditionnellement.
Singletons de module : pour la glu transversale, setPdfLogger(fn), setSpellHost(host) et setBibKeysProvider(fn) ont des valeurs par défaut no-op et sont installés par le shim de l’application comme effet de bord d’import.
Chaque extraction a laissé un fichier mince à l’ancien chemin @/… qui réexporte (ou encapsule) le package, afin que les consommateurs et les chemins vi.mock(...) existants ne changent jamais. Exemples : src/components/editor/cm/controller.ts (réexportation), src/components/pdf/PdfViewer.tsx (wrapper qui injecte l’ouvreur de liens du plugin shell), src/lib/ai-tools.ts (adaptateur + signatures d’origine de la fabrique). Le nouveau code peut importer @oleafly/* directement. Les shims existent pour la compatibilité, pas comme chemin privilégié.
Le registre de contributions
Section intitulée « Le registre de contributions »@oleafly/registry est la façon dont les fonctionnalités se branchent sur le shell de l’application sans que le shell les connaisse. Trois types de contributions :
- Onglets du rail (
registerRailTab) : icône, section (explore/review/assist, séparateurs tracés entre les sections non vides), visibilitéwhen(ctx)facultative, hookuseBadge()facultatif (par ex. le nombre de modifications Git), et le composantpanelde la barre latérale. - Commandes (
registerCommand) : servies à la palette de commandes (Cmd+K, groupées pargroup) et/ou à l’omnibar (Cmd+P) viasurfaces. Les libellés et les icônes peuvent être des fonctions du contexte (par ex. « Passer au thème clair »). - Ensembles d’outils IA (
registerAiToolset) : un mode de discussion ("chat","figure") associé à une fabrique d’ensemble d’outils. Le panneau de discussion recherche l’ensemble d’outils du mode actif au lieu d’en figer un dans le code.
AppContext ({ projectId, projectKind, theme }) est construit par chaque surface de rendu à partir de ses propres abonnements, pour que les conditions when/label restent réactives.
L’enregistrement est statique : src/contributions/ enregistre tous les éléments intégrés (onglets, commandes, ensembles d’outils) et main.tsx appelle registerContributions() une seule fois avant le montage du shell. Rail, Sidebar, CommandPalette, SearchOmnibar et ChatPanel n’affichent que ce qui est enregistré.
Pour ajouter l’onglet ou la commande d’une fonctionnalité, enregistrez-le. Aucune modification du shell :
// 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"),});C’est une API interne, pas un SDK de plugins : les formes peuvent évoluer librement, et rien n’est chargé dynamiquement. Reportés jusqu’à l’apparition d’un vrai consommateur : les formats du menu d’export, les schémas des réglages et les comportements par type de projet en tant que contributions.
Résolution des modules
Section intitulée « Résolution des modules »Comme les packages sont consommés comme code source, le même alias est déclaré à trois endroits, qui doivent tous rester synchronisés lors de l’ajout d’un package :
tsconfig.json→compilerOptions.paths. Les sous-chemins comme@oleafly/preflight/pdf-extractnécessitent leur propre entrée explicite.vite.config.ts→resolve.alias. Les alias de chaînes correspondent par préfixe, donc les sous-chemins se résolvent sans entrée supplémentaire.vitest.config.ts→ le mêmeresolve.alias, ettest.includedoit couvrirpackages/**/*.test.ts. Sinon, les tests déplacés disparaissent silencieusement de l’exécution.
À noter aussi : le package.json racine dépend de chaque package via "workspace:*", et src/styles/globals.css contient @source "../../packages" pour que Tailwind v4 analyse les sources des packages à la recherche de noms de classes.
Les imports réservés à Vite (workers ?worker&url, imports CSS) fonctionnent sans changement depuis les sources des packages parce que les alias pointent vers la source. Gardez tout ce qui tire pdf.js hors des barrels que des tests en environnement node importent : c’est pourquoi pdf-extract et la visionneuse du package preview ne sont accessibles que là où le DOM existe.
Travailler sur les packages
Section intitulée « Travailler sur les packages »- Tests : ils vivent à côté de la source (
packages/*/src/*.test.ts) et s’exécutent dans le mêmepnpm testque les tests de l’application. Surveillez le nombre de tests en déplaçant des fichiers. - Dépendances : elles sont déclarées de façon lâche (
"react": "*"). pnpm déduplique par rapport à la racine, où se trouvent les vrais planchers de versions. Ne déclarez pas de dépendances que le package n’importe pas. - Critères de validation pour tout changement de package :
pnpm exec tsc --noEmit,pnpm build,pnpm test(nombre inchangé) et le grep de pureté ci-dessus. - Extraire quelque chose de nouveau : déplacez les fichiers avec
git mv(l’historique suit), définissez les ports, laissez des shims aux anciens chemins, câblez les trois alias, puis exécutez les critères de validation. N’extrayez pas une fonctionnalité sur laquelle vous itérez activement. - Licence : chaque package est
AGPL-3.0-or-later, comme l’application.
Ce qui reste dans l’application
Section intitulée « Ce qui reste dans l’application »src/ garde tout ce qui constitue réellement l’application frontend : les stores Zustand, le client IPC Tauri (src/lib/tauri.ts), la bibliothèque UI shadcn, les composants de disposition/shell, les adaptateurs et shims des ports, src/contributions/, et la glu de fonctionnalités couplée aux stores par nature (l’interface du panneau de prévérification, le plugin de modifications IA en ligne, et l’intelligence de code ou de survol injectée dans l’éditeur comme extraExtensions). Les commandes Tauri et l’intégration au système d’exploitation restent dans src-tauri/. Les clients des fournisseurs, le streaming et l’orchestration de l’agent appartiennent à crates/oleafly-agent/ afin que les secrets et les requêtes aux fournisseurs restent hors du webview.