Skip to content

Architecture: packages, plugins and the shell

Architecture: packages, plugins and the shell

Section titled “Architecture: packages, plugins and the shell”

L’application est organisée en trois couches :

packages/ Données + logique pure (aucun kernel, aucun React)
plugins/ Couche d'adaptation : événements, handlers, UI déclarative
src/ (kernel+shell) Bus d'événements, store, transactions, surfaces UI

Le contrat des actions vient de @scfloworg/actions, copié depuis scflow-core : type, validation JSON Schema, registre, lots avec références ${nom.chemin}, export en outils IA et journal d’exécution. src/kernel/actions.ts (ActionManager) n’ajoute que ce qui relève de l’application : transaction du kernel, événements, dialogues et contexte par défaut. Le shell ne dépend que de ce paquet neutre, jamais du moteur MCL.

Un paquet est un module TypeScript autonome :

  • les données : types du modèle, valeurs initiales (référencés dans le store) ;
  • les traitements : fonctions pures modèle -> modèle, sélecteurs (référencés dans les handlers du plugin).

Un paquet ne connaît ni le kernel ni React : il est testable directement et réutilisable par plusieurs plugins. Convention : n’exporter que des fonctions et des types (les constantes de valeur ne passent pas par le pont de partage).

Exemple : packages/notes-core (modèle NotesModel + createDocument, changeContents, renameDocument, markSaved, closeDocument…).

Un plugin est la couche qui relie un ou plusieurs paquets au shell :

  • il déclare son UI dans ui.json (commandes, menus, toolbars, panneaux, réglages, traductions) et la charge avec ctx.ui.load("ui.json") ;
  • il enregistre les handlers qui adaptent les événements aux fonctions pures du paquet (slice.set((prev) => changeContents(prev, …))) ;
  • il ne crée des composants que lorsque les composants déclaratifs du shell ne suffisent pas (ex. l’éditeur de notes).

Quand un plugin dépendant réutilise un paquet déjà bundlé par un plugin dont il dépend, il ne doit pas le re-bundler (deux instances d’état/mémoire).

  • PL-A (propriétaire) bundlé le paquet A et expose l’instance : ctx.providePackage("A", A) dans setup().
  • PL-B (dépendant) déclare "dependsOn": ["pl-a"] et marque le paquet dans plugins/pl-b/plugin.json : { "shared": ["A"] }.
  • Le builder réécrit alors tous les imports de A dans le bundle de PL-B — y compris ceux faits à l’intérieur d’un paquet B bundlé — vers un pont paresseux qui lit l’instance enregistrée par PL-A (registre sur globalThis, partagé entre bundles).

Résolution paresseuse : le pont ne lit le registre qu’à l’appel, donc l’ordre d’évaluation des modules n’a pas d’importance ; dependsOn garantit que PL-A est actif avant le premier appel de PL-B. Si PL-A est désactivé, PL-B l’est aussi (désactivation en cascade).

provideApi / useApi restent le mécanisme pour les services de plugin (un plugin appelle une fonction d’un autre plugin) ; le partage de paquets ne concerne que les imports statiques de modules.

Démo : plugins/demo-shared-a + plugins/demo-shared-b avec packages/demo-greeter et packages/demo-farewell. Test : bun test tests/shared-packages.test.ts.

Documentation under CC BY 4.0 · © SCFlow contributors ·Back to supplychainflow.org