Aller au contenu

Code partagé

Les plateformes de l'écosystème sont différentes à l'extérieur, uniques à l'intérieur. Pour l'utilisateur, chacune a sa marque, son domaine et ses services — pourtant plus de 90 % du code provient d'une seule source.

Cette page explique comment ce code partagé est distribué.

Pourquoi cela est devenu nécessaire

Au début, chaque plateforme était copiée depuis le modèle. Résultat : une seule correction devait être répétée à la main dans 8 dépôts ; en oublier un, et cette plateforme prenait du retard sans que personne ne le voie.

La mesure a montré que ~95 % du frontend était réellement partagé ; la divergence réelle représentait environ 40 lignes portant le nom de la marque. Autrement dit, la duplication n'était pas une exigence technique mais un héritage de la copie.

Trois mécanismes

Le code partagé circule différemment selon la couche. Aucun de ces mécanismes n'est un « copier-coller » : tous sont versionnés et réversibles.

Couche Forme Mécanisme
Noyau backend module Go dépendance go.mod
Couche frontend paquet npm dépendance package.json
Squelette de plateforme historique git git merge + autosync quotidien

1. Noyau backend — un module Go

Authentification, contrôle d'accès par rôles (RBAC), passerelle API, audit, chaîne d'IA, intégration eID/SSO : tout cela vit dans un seul module Go. Le main.go d'une plateforme fait généralement une trentaine de lignes : démarrer le noyau, puis ajouter les routes propres à cette plateforme.

Le noyau comporte une seule couche directe :

  • open-gerege-core — la base ouverte consommée directement par tous les backends des lignes gouvernementale et Gerege.

Jusqu'au 2026-08-02, private-gerege-core se trouvait entre les deux. Comme il ne contenait ni logique supplémentaire ni migration, il a été retiré de la chaîne et archivé. La logique commerciale reste dans chaque dépôt produit.

2. Couche frontend — @gerege/ui-core

Le même problème résolu par le noyau côté backend, résolu à nouveau côté frontend. Le paquet contient :

  • lib/** — client d'API, utilitaires BFF, dictionnaire i18n, thème, session,
  • components/** — coquille, administration, espace utilisateur, eID, passerelle,
  • api/** — la logique de 158 routes BFF.

Le paquet est publié sous forme de code source TypeScript (non compilé) ; l'application le compile donc via transpilePackages de Next.js. La distribution se fait par tarball HTTPS ouvert : aucune authentification requise, fonctionne y compris dans une construction Docker.

Pourquoi les routes BFF conservent une enveloppe

Next.js enregistre les routes via le système de fichiers ; chaque application conserve donc une réexportation d'une ligne par chemin :

// src/app/api/org/[id]/route.ts
export { GET, PUT, DELETE } from '@gerege/ui-core/api/org/[id]';
export const dynamic = 'force-dynamic';

Les 158 fichiers pourraient se réduire à un unique [...path], mais cela détruirait une liste d'autorisation de sécurité : la liste des routes définit quels chemins du backend le navigateur peut atteindre. L'enveloppe est un prix délibéré.

3. Squelette de plateforme — héritage git

Ce qui n'appartient pas au paquet (structure des pages, globals.css, configuration de déploiement) est hérité du modèle par git merge. Un autosync quotidien récupère les changements du modèle amont et ouvre une pull request dans les dépôts applicatifs : tout changement atteignant la production passe par une revue humaine.

Les fichiers qui doivent rester propres à chaque plateforme (marque, déploiement, CI, documentation) sont protégés par merge=ours dans .gitattributes.

merge=ours ne protège pas des changements unilatéraux

Ce pilote ne résout que les conflits. Si le modèle amont supprime un fichier, la fusion le suit — le pilote n'est jamais invoqué. La vraie protection consiste à ce que chaque fichier de marque ou de configuration ait un contenu différent des deux côtés.

Ce qui reste la propriété de la plateforme

Dans le paquet / le noyau Propriété de la plateforme
lib/**, components/**, logique BFF brand.config.ts — nom, domaine, couleurs, URL de documentation
Authentification, RBAC, passerelle, audit components/landing/** — texte marketing
Intégration eID / SSO app/**/page.tsx — enregistrement des routes (enveloppes fines)
Dictionnaire i18n partagé (846 clés × 7 langues) lib/<platform>I18n.ts — la terminologie de la plateforme
Structure du menu (AppShell) nav.config.ts — les sections que la plateforme dessert
app/globals.css — jetons de couleur de marque
deploy/**, .github/** — déploiement, CI

Pourquoi la terminologie de plateforme reste dans l'application

La règle : le dictionnaire partagé ne connaît que la surface partagée. Les mots propres à une seule plateforme — le vocabulaire IBAN/relevés du portefeuille, le catalogue d'API du portail développeur, la terminologie des processus métier de Ring — résident dans le dictionnaire propre à l'application.

La raison en est le coût : placer les 1,104 termes de Ring dans le dictionnaire partagé, c'est les faire porter par kiosk, POS et le portefeuille — et chaque langue ajoutée multiplie ce coût par sept.

La mise en œuvre suit le même motif dans chaque dépôt :

// lib/walletI18n.ts — les 15 termes du portefeuille × 4 langues
export function useWalletT() {  }   // repli sur l'anglais pour les langues non traduites

Lorsqu'un composant transmet T en prop à ses composants enfants, le scinder en deux fonctions (T + wt) reviendrait à scinder chaque prop. Dans ce cas, on écrit un unique résolveur : si la clé appartient à la plateforme, on la prend dans son dictionnaire, sinon dans le paquet (lib/lang.ts de ring-dgov).

AppShell a la même organisation sur toutes les plateformes (Super admin · Admin · Manager · Citoyen), mais chaque plateforme n'en implémente qu'un sous-ensemble : le portefeuille n'a ni gateway, ni relay, ni modules de registre.

Réglage Rôle
navRoutes Les routes que l'application dessert réellement ; le menu est filtré par elles
navSystemLabels Le nom des systèmes dans le rail (me → « Portefeuille »)
navExtra Les entrées de menu propres à cette seule plateforme (les 21 entrées BPM de Ring)

navExtra provient d'un composant CLIENT

UiCoreProvider est un composant client appelé depuis le root layout serveur. Les icônes de menu (composants React) et les fonctions de libellé ne franchissent pas la frontière server→client. L'application crée donc une fine enveloppe client et les transmet depuis celle-ci :

// src/nav.config.tsx
'use client';
export default function AppNav({ children }) {
  return <UiCoreProvider navExtra={NAV_EXTRA}>{children}</UiCoreProvider>;
}

Une entrée de menu que peu de plateformes desservent peut être marquée optIn: true dans le paquet : elle n'apparaît alors que sur les plateformes qui l'ont explicitement déclarée dans navRoutes.

Trois garde-fous automatiques

Le code partagé crée trois types de dépendance différents. Chacun se manifeste autrement lorsqu'il casse ; les garde-fous sont donc trois eux aussi :

Dépendance Garde-fou Ce qui se passe en cas de casse
Code du paquet ← code de l'application tsc La compilation échoue — visible immédiatement
Route du paquet ← enveloppe BFF de l'application check-routes L'endpoint disparaît silencieusement
Classe du paquet ← CSS de l'application check-styles L'écran perd silencieusement son style
  • check-brand — la construction échoue si le nom d'une plateforme apparaît dans le code hors de brand.config.ts. Le nom de la plateforme est lu depuis brand.config.ts, la liste ne se périme donc pas à la main.

    Ce que le garde-fou a attrapé

    La page de connexion proposait de s'authentifier via « Gerege SSO (sso.gerege.mn) » alors que les plateformes de la ligne gouvernementale redirigent en réalité vers sso.dgov.mn. L'hôte est désormais lu depuis le SSO_ISSUER du backend.

  • check-routes — exige une enveloppe applicative pour chaque route du paquet. Sans elle, un nouvel endpoint du paquet disparaîtrait silencieusement sur cette plateforme (la logique n'étant pas visible dans l'application, rien ne semblerait cassé).

    Le garde-fou ne prouve pas que l'EXCLUDE est juste

    Une route délibérément non exposée s'inscrit dans EXCLUDE — l'écart devient explicite. Mais le garde-fou n'attrape pas une entrée écrite à tort. C'est ainsi que public/languages s'est retrouvé exclu sur une plateforme et que le sélecteur de langue est resté vide : tous les garde-fous au vert, l'écran cassé.

  • check-styles — le paquet ne contient pas de CSS : le style réside dans le globals.css de chaque dépôt. Lorsque le paquet nomme une nouvelle classe, ou que le CSS d'un dépôt vieillit, le composant perd silencieusement son style — le bouton retombe sur l'habillage gris par défaut du navigateur, le tableau perd ses bordures. Ce garde-fou confronte les className du paquet au CSS du dépôt.

Versionnage

Les trois mécanismes suivent le semver. Dès qu'une nouvelle version paraît, Dependabot ouvre une pull request dans les dépôts consommateurs ; la mise à jour elle-même est un changement d'une ligne dans go.mod ou package.json.

Relever la version du modèle ne suffit pas

Comme chaque plateforme hérite du modèle, on croit volontiers qu'« il suffit de relever le modèle pour que cela se propage partout ». En réalité, les deux types de dépendance ne fonctionnent pas de la même façon :

Fichier merge=ours ? Se propage depuis le modèle ?
backend/go.mod oui ❌ jamais
frontend/package.json non ✅ oui

La protection de go.mod est une exigence structurelle : la ligne module diffère dans chaque dépôt (…/gerege-app-mn/backend contre …/wallet-gerege-mn/backend), si bien que chaque fusion entrerait en conflit dès la première ligne. Relever la version du noyau backend dans le modèle ne propage donc rien : il faut une pull request par dépôt.

L'arbre d'héritage compte en outre trois étages (public template → private template → application) ; même un fichier capable de se propager met plusieurs cycles d'autosync à atteindre les feuilles.

Un changement cassant bloque les pull requests de dépendances

Passer le dictionnaire de quatre langues à sept a cassé chaque endroit où figurait Record<Lang, …>. Résultat : chaque pull request de Dependabot échouait au tsc, personne ne les fusionnait, la suivante s'empilait par-dessus — et la flotte s'est dispersée de la v0.4.0 à la v0.10.2.

Aussi, pour un changement cassant dans un paquet : (a) consigner les instructions de migration dans les notes de version, (b) livrer le correctif dans les dépôts consommateurs en même temps. Les mises à jour automatiques exigent une étape manuelle en cas de changement cassant.

Le retard est silencieux

Si les pull requests de dépendances s'accumulent, les plateformes s'éparpillent entre plusieurs versions et la promesse « une correction atteint tout le monde » se brise. Fermer régulièrement ces pull requests est une condition de fonctionnement de cette structure, non un agrément facultatif.

Voir aussi