Skip to content

Conventions ​

La plupart des règles du dépôt sont appliquées par l’outillage : le compilateur TypeScript, ESLint (eslint.config.js), Biome (biome.json) et des tests qui analysent le code. Quand une règle ne peut pas l’être, elle doit se vérifier en quelques secondes en revue. Cette page liste les règles et l’endroit où chacune est vérifiée.

Langue du code ​

Les identifiants, les noms de tables et de colonnes et les messages d’erreur techniques sont en anglais. Tout texte qu’un utilisateur peut lire passe par les traductions, en français et en anglais (voir Textes et traductions).

Un concept, un mot, partout : le code, la base, l’API et l’interface emploient les mêmes identifiants (member, mailbox, workflow, execution, step, scope, template…). N’introduisez pas de synonymes.

TypeScript ​

Les réglages partagés sont dans tsconfig.base.json :

  • strict, plus noUncheckedIndexedAccess et exactOptionalPropertyTypes : un accès indexé peut valoir undefined, et une propriété facultative ne reçoit pas un undefined explicite sauf si son type le prévoit ;
  • noImplicitReturns, noImplicitOverride, noFallthroughCasesInSwitch, noUnusedLocals, noUnusedParameters, useUnknownInCatchVariables ;
  • verbatimModuleSyntax, erasableSyntaxOnly (pas d’enum, pas de namespace : utilisez des objets as const), isolatedModules ;
  • des modules ES purs ("type": "module"), des imports relatifs avec leur extension .ts.

ESLint ajoute, en erreur :

RègleEffet
@typescript-eslint/no-explicit-anyany est interdit. Utilisez unknown et affinez ; validez les données externes avec zod.
@typescript-eslint/no-floating-promises, no-misused-promisesToute promesse est attendue ou explicitement gérée. Un « lancer sans attendre » assumé s’écrit void promesse.
@typescript-eslint/consistent-type-importsLes imports de types passent par import type ou type en ligne.
@typescript-eslint/no-unused-varsLes noms inutilisés sont des erreurs, sauf ceux qui commencent par _.
no-empty (blocs catch compris)Pas de catch muet : un catch traite l’erreur, ou l’enrichit et la relance.
no-consoleSeuls console.warn et console.error. Utilisez le journal.

Préférez les unions discriminées à plusieurs booléens (status: 'queued' | 'running' | …).

Frontières entre paquets ​

Les dépendances entre paquets sont strictement descendantes. eslint.config.js déclare, pour chaque paquet, la liste exhaustive des paquets @manko/* qu’il peut importer ; tout autre import est une erreur.

PaquetPeut importer
workflowrien du produit
api-typesworkflow
credentialsworkflow
nodesworkflow
connectorsworkflow, credentials
mirrorworkflow, connectors
engineworkflow, nodes
toolsworkflow
servertous les paquets ci-dessus
uiworkflow, api-types, nodes
apps/docsworkflow, api-types, nodes

ESLint refuse aussi :

  • les imports profonds dans un autre paquet (@manko/<paquet>/<chemin>) : un paquet n’expose que son point d’entrée ;
  • l’accès à un autre paquet par chemin relatif (../../autre/src/…) ;
  • toute dépendance à un paquet de l’écosystème n8n (n8n, n8n-*, @n8n/*). Copier du code n8n est également interdit, grammaires, chaînes de traduction et schémas compris.

Un nœud n’importe jamais un connecteur ni le SDK d’un fournisseur : les effets externes lui arrivent par les services de son contexte. Le même principe vaut ailleurs : la logique métier reçoit des interfaces (envoi de mail, service LLM, client HTTP, horloge, file) par injection, ce qui rend possibles les essais et les tests sans réseau.

Configuration et variables d’environnement ​

Seul le module de configuration typée, packages/server/src/config, lit process.env. ESLint refuse process.env partout ailleurs dans le code du produit. Les fichiers de test et le générateur de documentation en sont dispensés. Chaque variable est documentée dans .env.example et dans Variables d’environnement.

Bibliothèques cantonnées à un module ​

Certaines bibliothèques s’importent depuis un seul module, et ESLint les refuse ailleurs :

BibliothèqueSeul module autorisé
dompurify, jsdompackages/server/src/webmail-read/sanitize.ts (serveur), packages/ui/src/features/webmail/model/emailBody.ts (interface)
css-treepackages/server/src/webmail-read/css.ts

Tout nouveau chemin d’affichage d’un contenu externe (corps de mail) passe par ces modules d’assainissement.

Formatage ​

Biome est le seul formateur. pnpm format reformate, pnpm lint vérifie. Les réglages (biome.json) : espaces, indentation de 2, largeur de ligne de 100, fins de ligne LF, guillemets simples, points-virgules, virgules finales partout, parenthèses autour des paramètres de fonctions fléchées, imports organisés automatiquement. Le linter propre à Biome est désactivé : les règles de lint viennent d’ESLint. Dans l’interface, ESLint n’emploie que les règles essential du plugin Vue, pour ne jamais entrer en conflit avec Biome.

Le style ne se discute jamais en revue : le formateur tranche.

Erreurs et nouvelles tentatives ​

La distinction entre erreurs transitoires et définitives pilote les nouvelles tentatives : choisissez-la explicitement à chaque throw sur un appel externe.

OùClasses
Nœuds (packages/nodes/src/errors.ts)PermanentNodeError, RetryableNodeError, avec un code stable, un message en anglais, cause et details.
Moteur (packages/engine/src/errors.ts)TransientError, PermanentError, RateLimitedError (reprogrammée sans consommer de tentative).

Le moteur lit la classification par la propriété failureKind ; une erreur non classée est traitée comme transitoire. Chaînez l’erreur d’origine par cause, et ne mettez jamais le contenu d’un mail ni un secret dans details ou dans les messages.

Format d’erreur de l’API ​

Toutes les erreurs de l’API ont la même forme, définie par apiErrorSchema dans packages/api-types/src/errors.ts :

json
{ "code": "workflow.graph_outdated", "message": "technical message in English", "details": {} }
  • code est stable et fait partie du contrat : minuscules, chiffres, _ et . (^[a-z][a-z0-9_.]*$). Il ne se renomme jamais sans nouvelle version de l’API.
  • message est technique, en anglais, pour les journaux et le débogage. Le serveur n’envoie aucun texte d’interface.
  • details est un contexte structuré facultatif.

L’interface traduit le code : le message lisible vit sous errors.<code> dans packages/ui/src/i18n/locales/fr.json et en.json, avec un repli générique. Chaque corps de requête et de réponse a un schéma zod dans @manko/api-types, partagé par le serveur (validation) et l’interface (types).

Textes et traductions ​

Aucun texte en dur, et le français et l’anglais livrés ensemble, dans la même modification.

TexteOù il va
Interface (libellés, placeholders, title, aria-label, messages d’erreur)packages/ui/src/i18n/locales/fr.json et en.json
Libellés, descriptions et options des nœudsDans la définition du nœud, en { fr, en }
Résumés d’étape écrits par les nœuds pendant l’exécutionpackages/nodes/src/i18n/fr.ts et en.ts
Textes que le serveur écrit pour une personne (mail d’approbation, page de confirmation, résumé quotidien)packages/server/src/i18n/fr.ts et en.ts
Textes d’une intégration (nom, guide, champs)Dans la déclaration de l’intégration, en { fr, en }

Syntaxe des messages : {nom} interpole, et {compteur|singulier|pluriel} accorde selon la règle de pluriel de la langue (« 0 fichier » en français, « 0 files » en anglais). Écrivez une clé par phrase entière, jamais deux moitiés recollées ; une concaténation devient une clé paramétrée. Dates, durées, nombres et tailles se formatent avec Intl et la langue courante.

Des tests le font respecter :

  • packages/ui/src/i18n/hardcoded.test.ts analyse les sources de l’interface et échoue sur un texte de gabarit, un attribut non lié (title="…") ou un littéral français dans le code ;
  • packages/ui/src/i18n/i18n.test.ts vérifie que fr.json et en.json ont exactement les mêmes clés, et refuse une valeur anglaise identique à la française hors d’une liste d’exceptions (noms propres, codes) ;
  • packages/nodes/src/i18n/hardcoded.test.ts, packages/nodes/src/i18n/i18n.test.ts et packages/server/src/i18n/i18n.test.ts font de même pour les textes des nœuds et du serveur.

Les dispenses de fichier entier de l’analyse de l’interface sont réservées aux fichiers qui portent déjà les deux langues dans un catalogue local vérifié par le typeur (par exemple les messages des formulaires de nœud) ; une chaîne française sans son équivalent anglais n’est jamais dispensée.

Les fichiers de traduction sont partagés par de nombreuses modifications : ajoutez vos clés sous votre propre espace de noms et modifiez les fichiers, ne les réécrivez jamais en entier.

Les prompts envoyés aux modèles de langage s’écrivent en anglais dans le code. Ce sont des consignes données au modèle, pas du texte d’interface.

Nom du produit et domaines ​

Le nom affiché du produit vient de la variable de configuration BRAND_NAME, et l’adresse de l’instance de PUBLIC_BASE_URL. N’écrivez jamais le nom du produit ni un domaine dans le code. La documentation écrit le jeton Mankomail, remplacé au build, et son générateur refuse toute page qui contient le nom de code du produit.

Base de données et migrations ​

  • Les requêtes passent par Kysely, uniquement dans les repositories (un par agrégat, méthodes nommées d’après les cas d’usage). Les gabarits SQL bruts sont réservés aux repositories.
  • Les migrations sont des fichiers SQL bruts dans packages/server/migrations, nommés NNNN_nom.sql et appliqués dans l’ordre des numéros par l’exécuteur de migrations du serveur, sous un verrou consultatif. Une migration n’est jamais modifiée une fois fusionnée : son empreinte SHA-256 est stockée et vérifiée. Il n’existe pas de migration descendante ; une migration doit rester compatible avec la version précédente du code (étendre, puis resserrer).
  • Toute écriture sur plusieurs tables passe par une transaction explicite. Les invariants critiques sont portés par le schéma (contraintes d’unicité, clés étrangères), pas par le code.
  • Tout effet externe et toute création d’exécution passent par leur clé unique. Un conflit d’unicité sur cette clé est un résultat normal (sans effet), pas une erreur.

Règles de sécurité ​

  • Les secrets et les credentials ne sont jamais journalisés (le journal masque les chemins connus), jamais placés dans les données d’étape, jamais placés dans une URL.
  • Les jetons aléatoires sont stockés sous forme d’empreinte calculée par hashToken, et les secrets se comparent avec constantTimeEquals, tous deux dans packages/server/src/security/tokens.ts. Ne les réimplémentez pas.
  • Le contenu d’un mail n’atteint un modèle de langage que par le service LLM, et les prompts séparent les données des consignes.

Nœuds ​

Un nœud suit le contrat defineNode et rien d’autre : pas d’accès direct à la base, pas d’import de connecteur, uniquement les services de son contexte. Sa policy.effect est honnête, tout effet externe reçoit context.idempotencyKey, tout appel réseau respecte context.abortSignal. Le catalogue garde une seule version de chaque nœud, modifiée sur place. Le détail est dans Écrire un nœud.

Commits et pull requests ​

  • Les messages de commit suivent la forme Conventional Commits : type: sujet ou type(portée): sujet, avec des types comme feat, fix, refactor, docs, test. Aucun hook de commit ni linter de commit n’est installé : la forme se vérifie en revue.
  • Gardez des branches courtes et des pull requests petites (moins de 400 lignes environ hors tests et fichiers générés) ; découpez les modifications plus grandes.
  • Un bogue corrigé s’accompagne d’un test qui le reproduit, dans la même modification.
  • Une modification d’un comportement documenté met à jour la documentation dans la même modification, y compris ce site (sources des nœuds, guides d’intégration, pages).
  • Avant d’ouvrir une pull request, lancez pnpm typecheck, pnpm lint et les tests concernés. La CI exécute ensuite la vérification des types, le lint, la suite de tests complète avec PostgreSQL et MinIO, le build et un test fumigatoire de la pile en conteneurs (voir Contribuer).
  • La revue vérifie d’abord les invariants : idempotence, transactions, périmètre, effets déclarés.