Serveur et web
Un plugin ajoute une fonction à Hodi sans modifier le code de Hodi. Il a deux parties : une partie serveur, qui tourne sur le serveur de Hodi, et une partie web, qui s'affiche dans le navigateur. Cette page explique le rôle de chacune et comment elles communiquent.
Les deux parties d'un plugin
Le plus simple est de partir d'un exemple : le plugin Hello world des premiers pas. Ce plugin ajoute un bouton Dire bonjour dans Serveur › Plugins. Un clic sur ce bouton demande un message au serveur, puis le bouton affiche la réponse du serveur.
Voici qui fait quoi dans ce plugin :
- La partie web dessine le bouton. C'est le fichier
web/index.tsx. - La partie serveur fabrique le message. C'est le fichier
server/index.ts. - Hodi fait le lien : il transmet le clic au serveur, puis la réponse au bouton.
server/index.ts
web/index.tsx
Sur le serveur de Hodi, dans un bac à sable
Dans le navigateur, dans les pages de Hodi
Répondre aux routes, garder des données, lancer des tâches
Dessiner des écrans, des cartes et des boutons
ctx, le contexte que Hodi lui donne
hodi:sdk, les composants et les hooks de Hodi
Non
Non
Les fichiers d'un plugin
Un plugin est un dossier. Le plus simple pour démarrer est de copier le modèle de plugin. Ce modèle contient déjà tous les fichiers ci-dessous, le SDK, des tests et une CI, et ce modèle fonctionne tel quel.
plugin.json la carte d'identité du plugin
server/index.ts la partie serveur
web/index.tsx la partie web, une fois la personne connectée
web/public.tsx les écrans avant la connexion, s'il en a
locales/fr.json les textes en français
locales/en.json les textes en anglais
assets/ les images, copiées telles quelles
test/ les testsSeul plugin.json est obligatoire. Ce fichier donne le nom du plugin, sa version, ses réglages et ses permissions. Le manifeste décrit plugin.json champ par champ.
Du dossier au fichier installable
Hodi n'installe pas un dossier, mais un seul fichier .hodi-plugin. Cette commande fabrique le fichier .hodi-plugin, depuis le dossier du plugin :
npx hodi-plugin buildVoici ce que devient chaque fichier du dossier dans le fichier .hodi-plugin :
server/index.tsLe fichier devient server.js, avec le code des paquets npm qu'il importe.
web/index.tsxLe fichier devient web.js, et ses styles deviennent web.css.
web/public.tsxLe fichier devient web-public.js, et ses styles deviennent web-public.css.
plugin.jsonLe fichier est copié tel quel.
locales/Le dossier est copié tel quel, avec les textes de chaque langue.
assets/Le dossier est copié tel quel, avec les images et les autres fichiers.
Le fichier arrive dans dist/, par exemple dist/hello-world-0.1.0.hodi-plugin. Publier explique comment tester ce fichier, l'installer et le faire signer.
La partie serveur
Ce que fait la partie serveur
La partie serveur est le code du plugin qui tourne sur le serveur de Hodi, jamais dans le navigateur. La partie serveur sert à trois choses :
- répondre à la partie web, avec des routes ;
- garder des données, dans une base SQLite rien qu'à elle ;
- travailler toute seule, avec des tâches qui reviennent, comme un ménage chaque nuit.
Le fichier server/index.ts exporte un objet ServerPlugin. Voici la partie serveur complète de Hello world :
import type { ServerPlugin } from '@hodi-app/plugin-sdk/server'
const plugin: ServerPlugin = {
routes: {
'POST /hello': async (request, ctx) => {
const { greeting } = ctx.settings()
return { body: { text: greeting } }
}
}
}
export default pluginCe code déclare une seule route, POST /hello. Quand la partie web appelle cette route, la route lit le réglage greeting et répond { "text": "Bonjour" }.
Le bac à sable de la partie serveur
Le code d'un plugin peut venir de n'importe qui. Hodi ne lance donc pas le code d'un plugin avec son propre code, mais dans un bac à sable : un moteur JavaScript séparé, QuickJS, isolé du reste de Hodi.
Dans ce bac à sable, seul le JavaScript standard existe : JSON, Math, Date, les promesses, les classes… Les modules de Node.js n'y sont pas :
- pas de
fs, donc pas de lecture de fichier ; - pas de
fetch, donc pas d'appel direct à un site ; - pas de
process, ni derequire.
Pour tout le reste, la partie serveur passe par ctx, le contexte que Hodi donne à chaque route. Voici, pour cinq besoins courants, la fonction de ctx à utiliser :
Avec ctx.db, qui donne accès à sa base de données.
Avec ctx.settings(), qui renvoie la valeur choisie pour chaque réglage.
Avec ctx.fetchText(). Il faut la permission network.
Avec ctx.ai.ask(). Il faut la permission ai.
Avec ctx.log(), pour suivre ce que fait le plugin.
Hodi vérifie chaque appel : le plugin n'obtient que ce que ses permissions lui ouvrent. Le contexte serveur décrit tout ce que ctx contient.
Les paquets npm dans la partie serveur
La partie serveur peut importer des paquets npm. Pendant la construction du plugin, le code de ces paquets est copié dans server.js. Le bac à sable n'a donc rien à télécharger.
Un paquet npm ne marche dans le bac à sable que s'il est écrit en JavaScript standard. Un paquet qui a besoin de Node.js plante au premier appel, car Node.js n'existe pas dans le bac à sable. Voici quatre paquets connus, et la raison pour laquelle chacun marche ou non :
date-fnsOui. Il calcule des dates en JavaScript standard.
zodOui. Il vérifie des données en JavaScript standard.
sharpNon. Il redimensionne des images avec du code natif de Node.js.
pgNon. Il ouvre une connexion réseau vers PostgreSQL.
En cas de doute sur un paquet, npm test répond vite : les tests lancent le plugin dans le même bac à sable que Hodi.
Les limites du bac à sable
Un plugin ne doit jamais ralentir Hodi. Le bac à sable fixe donc des limites à la partie serveur. Voici chaque limite, et ce qui se passe quand la partie serveur la dépasse :
La partie serveur a 16 Mo, ou jusqu'à 32 Mo avec memoryMb. Au-delà, l'appel échoue.
Un appel dure 10 secondes au plus. Au-delà, Hodi l'arrête.
Une réponse fait 1 Mo au plus. Au-delà, l'appel échoue.
La base fait 256 Mo au plus. Au-delà, l'écriture échoue.
8 appels peuvent attendre derrière celui qui tourne. Au-delà, Hodi répond busy sans attendre.
Après 3 plantages en 10 minutes, le plugin s'arrête jusqu'au prochain démarrage de Hodi.
Le bac à sable s'endort après une minute sans appel, et se réveille à l'appel suivant. En s'endormant, le bac à sable oublie tout ce qui était en mémoire. Une variable globale ne garde donc pas sa valeur d'un appel à l'autre : ce qui doit durer va dans la base, avec ctx.db.
La partie web
Ce que fait la partie web
La partie web est le code du plugin qui tourne dans le navigateur, dans les pages de Hodi. La partie web dessine ce que la personne voit : une page dans Réglages, une carte, un bouton sous un article…
La partie web se charge en arrière-plan, une fois la personne connectée. L'app n'attend jamais un plugin pour s'afficher : le plugin apparaît dès qu'il est prêt.
Voici la partie web de Hello world. Cette partie web dessine une carte avec un bouton Dire bonjour, sur la page du plugin dans Serveur › Plugins. Un clic sur le bouton appelle la route /hello, puis la carte affiche la réponse de la route :
import { Button, SettingRow, definePlugin, usePluginApi } from 'hodi:sdk'
import { useState } from 'react'
const HelloCard = () => {
const api = usePluginApi()
const [text, setText] = useState('')
const sayHello = async () => {
const answer = await api('/hello', { method: 'POST' })
setText((await answer.json()).text)
}
return (
<SettingRow label="Dire bonjour" hint={text}>
<Button onClick={sayHello}>Dire bonjour</Button>
</SettingRow>
)
}
definePlugin({ settingsPage: HelloCard })Ce que la partie web importe
La partie web importe tout de hodi:sdk : les composants de Hodi (Button, SettingRow…), les hooks de Hodi (usePluginApi…) et definePlugin. Ces composants ne sont pas des copies : ce sont les composants de la page de Hodi elle-même. Le plugin a donc l'air du reste de l'app, dans chaque thème et sur chaque taille d'écran.
Voici où trouver chaque sujet de la partie web :
- Où le plugin s'affiche :
definePlugin()le dit, voir les emplacements. - Les composants : ceux de l'app, listés dans Composants.
- Les textes :
usePluginTranslation()les lit dans le dossierlocales/. - Les styles : un fichier CSS, avec des classes préfixées par l'id du plugin et les tokens de Hodi.
Les styles de la partie web
La page de Hodi refuse les styles en ligne, comme style={{ color: 'red' }}. Chaque style passe donc par une classe, dans un fichier CSS importé par web/index.tsx, comme web/plugin.css dans le modèle.
.hello-world-answer {
color: var(--text-2);
}Les couleurs viennent des variables de Hodi, comme var(--text-2). Ces variables changent avec le thème (Clair, Sombre, Poudré). Une couleur écrite en dur, comme #333, ne change pas et devient illisible dans un autre thème. Tokens liste toutes ces variables : couleurs, polices, arrondis et mouvement.
Quand Hodi affiche la partie web
La partie web tourne dans la page de Hodi, avec la session de la personne connectée. Pour cette raison, Hodi n'affiche la partie web d'un plugin que dans deux cas :
- Le plugin est signé par le mainteneur de Hodi, après relecture de son code.
- Le plugin est en développement, envoyé par
hodi-plugin devsur un Hodi de test.
Un plugin non signé s'installe quand même, mais sans sa partie web. Publier explique comment faire signer un plugin.
Appeler la partie serveur
La partie web appelle une route de la partie serveur par le chemin de cette route, avec usePluginApi(). Hodi se place au milieu : Hodi vérifie qui demande, puis passe la requête à la partie serveur.
const api = usePluginApi()
const answer = await api('/hello', { method: 'POST' })
const { text } = await answer.json()Voici ce qui se passe, dans l'ordre, pendant cet appel :
- Le navigateur demande
/api/plugins/hello-world/hello. - Hodi vérifie que la personne est connectée.
- Hodi vérifie que le plugin est prévu pour son profil.
- Hodi appelle la route
'POST /hello'du plugin. - La route reçoit
request.who: qui demande. - La route ne reçoit aucun cookie.
- Sa réponse revient à la partie web, en JSON.
request.who dit qui demande, tel que Hodi l'a vérifié : le profil de la personne, son foyer, si la personne est adulte, ado ou enfant, et si elle est admin. La route n'a donc rien à vérifier elle-même. La route ne reçoit jamais de cookie ni de jeton : le plugin ne peut pas se faire passer pour la personne.
Les textes du plugin
Chaque texte que la personne lit dans un plugin vient des fichiers locales/ du plugin. locales/fr.json est la référence, et chaque clé existe aussi dans locales/en.json. Les textes français d'un plugin tutoient la personne, comme le reste de Hodi.
{
"manifest": {
"name": "Hello world",
"description": "Un réglage, une route et sa propre carte."
},
"settings": {
"greeting": "Message",
"greetingHint": "Ce que le plugin répond"
},
"say": "Dire bonjour"
}Le fichier locales/fr.json a trois sortes de clés :
manifest: le nom et la description du plugin dans cette langue.settings: le libellé de chaque réglage, et sa ligne d'aide.- Les autres : les textes de la partie web, lus avec
usePluginTranslation().
La partie web affiche un texte avec la clé de ce texte :
const { t } = usePluginTranslation()
<Button onClick={sayHello}>{t('say')}</Button>Avant de commencer
Avant d'écrire un plugin, quatre choses sont à préparer :
- Installer Node.js 24 ou plus récent.
- Copier le modèle de plugin.
- Lancer
npm installdans son dossier. - Préparer un Hodi de test, avec Serveur › Plugins › Mode développeur activé.
Le modèle contient déjà le SDK, @hodi-app/plugin-sdk : npm install l'installe avec le reste. Sans le modèle, il s'ajoute avec npm install --save-dev @hodi-app/plugin-sdk.
Les premiers pas montrent ces étapes en vrai, dans un éditeur : un plugin très simple, en une dizaine de minutes. Ce plugin est un exemple guidé pour découvrir les étapes : il ne sert à rien au quotidien.