Skip to content

GeoLeaf-JS — Architecture Guide

Package : @geoleaf/core

Version : 2.0.0

Target : Browser (ESM), TypeScript strict


Table des matières

  1. Vue d'ensemble
  2. Séquence de boot B1→B11
  3. Formats de bundle
  4. Adaptateur cartographique IMapAdapter
  5. Modules built-in vs optional
  6. Système de lazy loading
  7. Façades publiques (GeoLeaf.*)
  8. Couche shim
  9. Pattern Plugin Registry
  10. Module de sécurité
  11. État partagé
  12. Règle no-premium-in-core

Vue d'ensemble

GeoLeaf-JS est une bibliothèque TypeScript de cartographie interactive construite sur MapLibre GL JS ^5.0.0. Elle est structurée en monorepo (npm workspaces + Turborepo) avec trois packages :

PackageLicenceRegistre
@geoleaf/coreMITnpmjs.org (public)
@geoleaf-plugins/storageCommercialnpm.pkg.github.com
@geoleaf-plugins/addpoiCommercialnpm.pkg.github.com

Le package core expose :

  • Un bundle ESM CDN (geoleaf.esm.js, assigne window.GeoLeaf.*) pour usage CDN/navigateur
  • Un bundle ESM avec 31 exports nommés pour les bundlers (Vite, webpack, etc.)
  • Un bundle Lite (ESM sans Table, Labels, Route, Search) pour les déploiements allégés
  • 11 chunks ESM lazy-loadés pour le code splitting

Séquence de boot B1→B11

La séquence de boot est orchestrée par src/modules/globals.ts, qui importe les sous-modules de domaine dans un ordre strict. Cet ordre est critique — ne jamais le modifier sans comprendre toutes les dépendances aval.

bundle-esm-entry.ts

    └── globals.ts  (orchestrateur — imports dans l'ordre)

            ├── B1+B2  globals.core.ts
            │     ├── B1: Log, Errors, CONSTANTS, Security, CSRFToken
            │     └── B2: Utils (AnimationHelper, DOMSecurity, ErrorLogger,
            │               EventHelpers, EventListenerManager, EventBus,
            │               FetchHelper, FileValidator, MapHelpers,
            │               PerformanceProfiler, LazyLoader, TimerManager,
            │               ObjectUtils, ScaleUtils)

            ├── B3+B4  globals.config.ts
            │     ├── B3: Helpers, Validators
            │     └── B4: Renderers, Data, Loaders, Map, Config

            ├── B5     globals.geojson.ts
            │     └── B5: GeoJSON (INTERNE), Route

            ├── B6+B7+B9  globals.ui.ts
            │     ├── B6: Labels
            │     ├── B7: Legend, LayerManager
            │     └── B9: Themes, UI, Controls, Filters

            ├── B8     globals.storage.ts
            │     └── B8: Namespace Storage (peuplé par le plugin à runtime)

            ├── B10    globals.poi.ts
            │     └── B10: POI, AddForm, Renderers

            └── B11    globals.api.ts  ← DOIT ÊTRE EN DERNIER
                  └── B11: Toutes les façades publiques (Core, GeoLeafAPI, POI, Route, Table, UI,
                            Filters, Baselayers, Legend, LayerManager, Helpers,
                            Validators, Themes, Labels, Geocoding, Search, Permalink,
                            Events, Notifications, PWA) + PluginRegistry + BootInfo


    app/helpers.ts  (helpers au boot)
    app/init.ts     (orchestrateur d'initialisation)
    app/boot.ts     (boot principal — s'exécute après globals)

ModuleRegistry (Sprint 3+)

En complément des globals, app/boot.ts instancie un ModuleRegistry qui gère le cycle de vie des modules via un graphe de dépendances :

boot.ts

    └── new ModuleRegistry()
            ├── register(new SecurityModule())
            ├── register(new CoreMapModule())
            ├── register(new ConfigModule())
            ├── register(new SharedModule())
            ├── register(new GeoJSONModule())
            ├── register(new UIModule())
            ├── register(new POIModule())
            ├── register(new APIModule())

            │   [après loadConfig() — modules optionnels selon profil]
            ├── register(new RouteModule())       si route.enabled !== false
            ├── register(new LabelsModule())      si labels.enabled !== false
            ├── register(new LegendModule())      si ui.showLegend !== false
            ├── register(new TableModule())       si ui.showTable !== false
            └── register(new SearchModule())      si ui.showSearch !== false

Le tri topologique (algorithme de Kahn BFS) garantit l'ordre d'initialisation en respectant les dépendances déclarées dans chaque module via ICoreModule.dependencies.

Règles critiques

  • globals.api.ts (B11) doit être en dernier — il lit les façades enregistrées par B1–B10.
  • globals.core.ts (B1+B2) doit être en premier — tous les modules dépendent de Log et Errors.
  • globals.storage.ts (B8) configure le namespace GeoLeaf.Storage — le plugin commercial le peuple à runtime.
  • Ne jamais modifier l'ordre de chargement sans lire tous les fichiers consommateurs.

Formats de bundle

ESM CDN (bundle-esm-entry.tsgeoleaf.esm.js)

Produit dist/geoleaf.esm.js (bundle flat CDN). Assigne window.GeoLeaf.* au chargement via les side-effects de globals.ts.

Usage (CDN/navigateur) :

html
<script type="module" src="https://unpkg.com/@geoleaf/core@2.0.0/dist/geoleaf.esm.js"></script>
<!-- window.GeoLeaf est maintenant disponible -->

ESM (bundle-esm-entry.ts)

Produit dist/esm/bundle-esm-entry.js + dist/chunks/ (11 lazy chunks). Exporte 31 symboles nommés importables par les consommateurs TypeScript/ESM. Déclenche le même boot side-effect.

Usage (bundler/npm) :

ts
import { Core, POI, UI, Filters } from "@geoleaf/core";

Lite (bundle-core-lite-entry.ts)

Variante ESM Lite sans les modules Table, Labels et Route. Vise une réduction de ~30% de la taille du bundle pour les déploiements qui n'ont pas besoin de ces fonctionnalités. Utilise boot-lite.ts et globals.api-lite.ts à la place de leurs équivalents complets.


Adaptateur cartographique IMapAdapter

GeoLeaf V2 abstrait totalement le moteur cartographique derrière l'interface IMapAdapter (définie dans src/contracts/map-adapter.contract.ts). Aucun module métier ne doit importer directement depuis maplibre-gl.

Types géographiques

TypeDéfinitionUsage
GeoLeafLatLng{ lat, lng } (WGS 84)Coordonnée ponctuelle
GeoLeafBounds{ north, south, east, west }Emprise géographique
GeoLeafPoint{ x, y } (pixels, origine haut-gauche)Projection écran

Convention d'ordre : GeoLeaf utilise { lat, lng } ; MapLibre GL utilise [lng, lat] (ordre GeoJSON). La conversion est exclusivement dans l'adaptateur.

Surface de l'interface IMapAdapter

typescript
interface IMapAdapter {
    // Initialisation
    init(options: MapInitOptions): void;
    isReady(): boolean;
    destroy(): void;

    // Vue / Navigation
    setView(center: GeoLeafLatLng, zoom: number): void;
    getCenter(): GeoLeafLatLng;
    getZoom(): number;
    setZoom(zoom: number): void;
    panTo(center: GeoLeafLatLng): void;
    flyTo(center: GeoLeafLatLng, zoom?: number): void;
    fitBounds(bounds: GeoLeafBounds, options?: { padding?: GeoLeafPoint; animate?: boolean }): void;
    getBounds(): GeoLeafBounds;

    // Événements (set normalisé)
    on(event: MapEvent, handler: (e: unknown) => void): void;
    off(event: MapEvent, handler: (e: unknown) => void): void;
    once(event: MapEvent, handler: (e: unknown) => void): void;

    // Couches GeoJSON
    addGeoJSONLayer(id: string, data: unknown, options?: GeoLeafLayerOptions): void;
    removeLayer(id: string): void;
    hasLayer(id: string): boolean;
    showLayer(id: string): void;
    hideLayer(id: string): void;
    updateLayerData(id: string, data: unknown): void;
    setLayerStyle(id: string, style: GeoLeafStyleOptions): void;
    setLayerFilter(id: string, filter: unknown): void;

    // Marqueurs
    createMarker(id: string, position: GeoLeafLatLng, options?: GeoLeafMarkerOptions): void;
    removeMarker(id: string): void;
    updateMarkerPosition(id: string, position: GeoLeafLatLng): void;
    createClusterGroup(id: string, options?: Record<string, unknown>): void;

    // Popups (handles opaques)
    createPopup(content: string | HTMLElement, options?: GeoLeafPopupOptions): unknown;
    openPopup(popup: unknown, position?: GeoLeafLatLng): void;
    closePopup(popup?: unknown): void;

    // Contrôles
    addControl(control: unknown, position: GeoLeafControlPosition): GeoLeafControl;
    removeControl(control: GeoLeafControl): void;

    // Utilitaires
    latLngToPoint(latlng: GeoLeafLatLng): GeoLeafPoint;
    pointToLatLng(point: GeoLeafPoint): GeoLeafLatLng;
    getContainer(): HTMLElement;
}

L'implémentation concrète est MaplibreAdapter dans src/adapters/maplibre/.


Modules built-in vs optional

Modules built-in (toujours présents)

Chargés dans le bundle ESM (geoleaf.esm.js). Aucun import réseau supplémentaire nécessaire.

Module (window.GeoLeaf.*)Source (src/modules/)Description
Logutils/log/Système de log interne
Errorsutils/errors/9 classes d'erreur typées
CONSTANTSutils/constants/Constantes globales
Securitysecurity/Sanitisation XSS, CSRF, DOM
Utilsutils/general/~15 utilitaires (fetch, animation, lazy, perf…)
Configbuilt-in/config/Chargement profil, taxonomie, normalisation
Coregeoleaf.core.tsCréation carte MapLibre, couches de base
Baselayersgeoleaf.baselayers.tsFonds de plan raster et vecteur
Filtersgeoleaf.filters.tsSystème de filtres
UIgeoleaf.ui.ts + ui/Interface (notifications, filtres, contrôles)
Helpersgeoleaf.helpers.tsHelpers publics
Validatorsgeoleaf.validators.tsValidation de données
pluginsbuilt-in/api/plugin-registry.tsGeoLeaf.plugins.* — PluginRegistry
bootInfobuilt-in/api/boot-info.tsToast de démarrage

Modules optional (lazy chunks)

Chargés à la demande via GeoLeaf._loadModule(). Produisent des chunks séparés dans dist/chunks/.

Clé _loadModule()Fichier sourceDescription
poipoi-corepoi-renderers + poi-extrasPOI complet (ordre garanti)
poiCorelazy/poi-core.tsAffichage marqueurs POI
poiRendererslazy/poi-renderers.tsRenderers popup/sidepanel
poiExtraslazy/poi-extras.tsSidepanel extras
basemapSelectorlazy/basemap-selector.tsSélecteur fond de plan
routelazy/route.tsCouches itinéraires
layerManagerlazy/layer-manager.tsPanneau gestionnaire couches
legendlazy/legend.tsPanneau légende
labelslazy/labels.tsSystème de labels dynamiques
themeslazy/themes.tsSystème de thèmes
tablelazy/table.tsTableau de données
searchlazy/search.tsRecherche plein texte

Système de lazy loading

GeoLeaf._loadModule(moduleName)

Charge un module secondaire à la demande :

js
await GeoLeaf._loadModule("poi"); // Charge poi-core + poi-renderers + poi-extras
await GeoLeaf._loadModule("route"); // Charge le chunk route
await GeoLeaf._loadModule("legend"); // Charge le chunk légende
await GeoLeaf._loadModule("table"); // Charge le chunk tableau
await GeoLeaf._loadModule("themes"); // Charge le chunk thèmes
await GeoLeaf._loadModule("labels"); // Charge le chunk labels
await GeoLeaf._loadModule("layerManager"); // Charge le gestionnaire de couches
await GeoLeaf._loadModule("search"); // Charge la recherche
await GeoLeaf._loadModule("basemapSelector");

Chargement granulaire POI :

js
await GeoLeaf._loadModule("poiCore"); // Core POI uniquement
await GeoLeaf._loadModule("poiRenderers"); // Renderers POI uniquement
await GeoLeaf._loadModule("poiExtras"); // Extras POI uniquement

GeoLeaf._loadAllSecondaryModules()

Charge tous les modules secondaires en parallèle (sauf basemapSelector). Le core POI se charge en premier — poi-renderers et poi-extras en dépendent.

js
await GeoLeaf._loadAllSecondaryModules();
// Tous les modules secondaires sont maintenant disponibles sur window.GeoLeaf

Façades publiques (GeoLeaf.*)

L'API publique est exposée à travers 16+ fichiers façade dans src/modules/. Chaque façade ré-exporte depuis l'implémentation du module de domaine — aucune logique métier dans les façades.

Namespace global (window.GeoLeaf.*)

Après le boot, window.GeoLeaf contient :

PropriétéTypeDescription
GeoLeaf.CoreObjectInit carte, thèmes, cycle de vie
GeoLeaf.POIObjectMarqueurs POI, clustering, popup
GeoLeaf.UIObjectContrôles, panneaux, filtres UI
GeoLeaf.FiltersObjectPanneaux filtres, recherche texte/catégorie/géo
GeoLeaf.RouteObjectRoute/itinéraires
GeoLeaf.TableObjectTableau de données
GeoLeaf.LegendObjectPanneau légende
GeoLeaf.LayerManagerObjectGestion couches GeoJSON
GeoLeaf.BaselayersObjectFonds de plan (raster + vecteur MapLibre)
GeoLeaf.HelpersObjectHelpers utilitaires
GeoLeaf.ValidatorsObjectValidateurs d'entrée
GeoLeaf.ThemesObjectGestion des thèmes
GeoLeaf.LabelsObjectSystème de labels
GeoLeaf.NotificationsObjectSystème de notifications toast
GeoLeaf.PermalinkObjectPermalink URL
GeoLeaf.EventsObjectBus d'événements
GeoLeaf.SearchObjectRecherche plein texte
GeoLeaf.GeocodingObjectRecherche d'adresse / géocodage
GeoLeaf.PWAObjectProgressive Web App (install prompt)
GeoLeaf.ConfigObjectAccès à la configuration (get/set)
GeoLeaf.UtilsObject~15 sous-modules utilitaires
GeoLeaf.CONSTANTSObjectConstantes applicatives
GeoLeaf.LogObjectSystème de log
GeoLeaf.ErrorsObject9 classes d'erreur typées
GeoLeaf.SecurityObjectHelpers XSS/CSRF
GeoLeaf.StorageObjectNamespace Storage (peuplé par le plugin à runtime)
GeoLeaf.pluginsObjectRequête/enregistrement plugins (PluginRegistry)
GeoLeaf.registryObjectModuleRegistry public (auto-enregistrement tiers)
GeoLeaf._loadModuleFunctionLazy load d'un module secondaire
GeoLeaf._loadAllSecondaryModulesFunctionCharge tous les modules secondaires
GeoLeaf._versionstringVersion courante (ex. "2.0.0")

Note : GeoLeaf.GeoJSON n'est pas une façade publique. Les couches GeoJSON sont gérées en interne et accessibles via GeoLeaf.LayerManager et les profils JSON.

Exports nommés ESM (31)

Depuis bundle-esm-entry.ts :

ts
// Façades haut niveau
export { Core } from "./modules/geoleaf.core.js";
export { GeoLeafAPI } from "./modules/geoleaf.api.js";
export { UI } from "./modules/geoleaf.ui.js";
export { POI } from "./modules/geoleaf.poi.js";
export { Route } from "./modules/geoleaf.route.js";
export { Table } from "./modules/geoleaf.table.js";
export { Legend } from "./modules/geoleaf.legend.js";
export { LayerManager } from "./modules/geoleaf.layer-manager.js";
export { Filters } from "./modules/geoleaf.filters.js";
export { Baselayers } from "./modules/geoleaf.baselayers.js";
export { Helpers } from "./modules/geoleaf.helpers.js";
export { Validators } from "./modules/geoleaf.validators.js";
export { Themes } from "./modules/geoleaf.themes.js";
export { Permalink } from "./modules/geoleaf.permalink.js";
export { Events } from "./modules/geoleaf.events.js";
export { Search } from "./modules/geoleaf.search.js";
export { Notifications } from "./modules/geoleaf.notifications.js";
export { PWA } from "./modules/geoleaf.pwa.js";
export { Geocoding } from "./modules/geoleaf.geocoding.js";

// Sous-modules API
export {
    APIController,
    APIFactoryManager,
    APIInitializationManager,
    APIModuleManager,
    PluginRegistry,
    BootInfo,
    showBootInfo,
} from "./modules/built-in/api/index.js";

// Utilitaires core
export { Log } from "./modules/utils/log/index.js";
export { Errors } from "./modules/utils/errors/index.js";
export { CONSTANTS } from "./modules/utils/constants/index.js";
export { Utils } from "./modules/utils/general/general-utils.js";
export { Config } from "./modules/built-in/config/geoleaf-config/config-core.js";

Couche shim

Les répertoires de premier niveau suivants dans src/ sont des shims de compatibilité ascendante — de simples ré-exporteurs qui redirigent vers src/modules/. Ils ne contiennent aucune logique et ne doivent pas être modifiés directement.

src/baselayers/   → src/modules/baselayers/
src/filters/      → src/modules/filters/
src/geojson/      → src/modules/geojson/
src/helpers/      → src/modules/helpers/
src/layers/       → src/modules/layer-manager/ (ou geojson/)
src/markers/      → src/modules/poi/
src/poi/          → src/modules/poi/
src/storage/      → src/modules/storage/
src/themes/       → src/modules/themes/
src/ui/           → src/modules/ui/
src/validators/   → src/modules/validators/

Toute l'implémentation réelle se trouve exclusivement dans src/modules/.


Pattern Plugin Registry

GeoLeaf utilise un pattern d'enregistrement explicite pour maintenir une séparation stricte entre le core MIT et les plugins commerciaux.

Enregistrement (côté plugin)

ts
// Dans le point d'entrée du plugin (ex. plugin-storage/src/entry.ts)
import { PluginRegistry } from "@geoleaf/core";

PluginRegistry.register("storage", {
    version: "2.0.0",
    requires: ["core"],
    label: "GeoLeaf Storage",
});

Requête (côté consommateur)

js
GeoLeaf.plugins.isLoaded("storage"); // → true/false
GeoLeaf.plugins.getLoadedPlugins(); // → ["core", "storage"]
GeoLeaf.plugins.canActivate("addpoi"); // → true si dépendances OK
GeoLeaf.plugins.getAvailableModules(); // → tous les modules (chargés + lazy)
GeoLeaf.plugins.getInfo("storage"); // → { name, version, loaded, loadedAt, … }
await GeoLeaf.plugins.load("layerManager"); // → lazy load depuis le registre

Règle no-premium-in-core

packages/core/src/ ne doit jamais importer @geoleaf-plugins/*. Vérification via scripts/verify-no-premium-in-core.cjs (exécuté en CI).


Module de sécurité

Toute injection DOM doit passer par src/modules/security/. Ne jamais utiliser innerHTML directement dans le code applicatif.

ts
import { Security } from "@geoleaf/core";

// via CDN (window.GeoLeaf):
GeoLeaf.Security.sanitize(htmlString); // Sanitisation XSS
GeoLeaf.Security.CSRFToken.get(); // Helper token CSRF

Utilitaires clés :

  • Security.sanitize() — Sanitisation XSS (supprime le HTML dangereux)
  • DOMSecurity — Helpers DOM sécurisés
  • CSRFToken — Gestion du token CSRF
  • src/modules/utils/dom-security.ts — Opérations DOM sécurisées

État partagé

src/modules/shared/ contient les objets d'état cross-modules. Avant de modifier un fichier ici, identifier tous les consommateurs — les changements d'état partagé peuvent silencieusement casser plusieurs modules.

Fichiers d'état partagé clés :

  • poi-state.ts — Sélection POI, marqueurs actifs
  • geojson-state.ts — Registre des couches GeoJSON chargées

Règle no-premium-in-core

packages/core/src/ doit avoir zéro référence à @geoleaf-plugins/*.

Vérification :

bash
node scripts/verify-no-premium-in-core.cjs

Ce script s'exécute en CI et fait échouer le build si un import premium est détecté. Les fonctionnalités commerciales sont injectées dans le namespace GeoLeaf par les plugins à runtime, via le pattern Plugin Registry.


Dernière mise à jour : mars 2026

Version GeoLeaf : 2.0.0 — Platform V2

Released under the MIT License.