How Built‑In Plugins Register with the GeoLibre PluginManager and What Lifecycle Hooks Are Available

Built‑in plugins in GeoLibre register automatically at application startup through a React hook that iterates over a static map of plugins and calls pluginManager.registerPlugin(), then respond to six lifecycle hooks defined in the Plugin interface: onRegister, onActivate, onDeactivate, onUnload, onUpdate, and render‑time hooks onBeforeRender/onAfterRender.

The GeoLibre mapping platform implements a modular plugin architecture in its @geolibre/plugins workspace. This system allows both built‑in and external extensions to integrate cleanly with the MapLibre‑based renderer while maintaining predictable initialization and cleanup behavior.

How Built‑In Plugins Register with PluginManager

Built‑in plugin registration follows a three‑stage pipeline that separates definition, aggregation, and runtime registration.

Plugin Definition in Individual Modules

Each built‑in plugin is authored as a TypeScript module under packages/plugins/src/plugins/. Every module exports a default object conforming to the Plugin interface defined in packages/plugins/src/types.ts. These modules contain the plugin's lifecycle implementations and metadata.

Aggregation in the Built‑In Plugins Map

The central index file packages/plugins/src/index.ts imports all individual plugins and constructs the builtInPlugins map. This static structure ensures the PluginManager receives a complete, type‑safe registry of available extensions.

// packages/plugins/src/index.ts
import { osmBasemap } from "./plugins/osm-basemap";
import { rasterLayerSync } from "./plugins/raster-layer-sync";
// …additional plugin imports

export const builtInPlugins = {
  osmBasemap,
  rasterLayerSync,
  // …all other built‑in plugins
} as const;

PluginManager Singleton and Registration Methods

The PluginManager class in packages/plugins/src/plugin-manager.ts implements a singleton pattern. It maintains an internal Map<string, Plugin> registry and exposes methods for lifecycle management.

// packages/plugins/src/plugin-manager.ts
export class PluginManager {
  private plugins = new Map<string, Plugin>();

  registerPlugin(id: string, plugin: Plugin): void {
    this.plugins.set(id, plugin);
    // Triggers onRegister hook if implemented
  }

  unregisterPlugin(id: string): void {
    // Triggers onUnload hook before removal
    this.plugins.delete(id);
  }

  activatePlugin(id: string, context: PluginContext): void {
    // Triggers onActivate hook
  }

  deactivatePlugin(id: string, context: PluginContext): void {
    // Triggers onDeactivate hook
  }

  getPlugin(id: string): Plugin | undefined {
    return this.plugins.get(id);
  }
}

export const pluginManager = new PluginManager();

React Hook‑Based Registration at Startup

The desktop application initiates registration through apps/geolibre-desktop/src/hooks/usePlugins.ts. This hook executes once during component mount, iterating over builtInPlugins and invoking registerPlugin() for each entry.

// apps/geolibre-desktop/src/hooks/usePlugins.ts
import { useEffect } from "react";
import { builtInPlugins } from "@geolibre/plugins";
import { pluginManager } from "@geolibre/plugins/plugin-manager";

export function usePlugins() {
  useEffect(() => {
    Object.entries(builtInPlugins).forEach(([id, plugin]) => {
      pluginManager.registerPlugin(id, plugin);
    });
  }, []); // Empty dependency array ensures single execution
}

This pattern guarantees that:

  • All built‑in plugins exist in the registry before any UI component attempts activation
  • Registration occurs exactly once per application session
  • External plugins added later follow the same interface contract

Lifecycle Hooks Available to GeoLibre Plugins

The Plugin interface in packages/plugins/src/types.ts specifies optional callback methods that the PluginManager invokes at defined execution points. All hooks receive a PluginContext object containing the Zustand store, MapLibre map instance, API helpers, and logging utilities.

Core Lifecycle Hooks

Hook Invocation Timing Typical Implementation
onRegister(context) Immediately after registerPlugin() completes Initialize internal state; attach global event listeners needed regardless of active status
onActivate(context) When user enables plugin or auto‑activation occurs Create MapLibre sources and layers; start background workers; allocate GPU resources
onDeactivate(context) When user disables plugin Remove map layers and sources; stop timers; detach UI components
onUnload(context) Application shutdown or permanent plugin removal Release all remaining resources; deregister global listeners; persist final state
onUpdate(prevConfig, newConfig, context) Configuration change detected Re‑apply settings to running map sources; restart processes with new parameters

Render‑Synchronization Hooks

Hook Invocation Timing Typical Implementation
onBeforeRender(context) Prior to each map render frame Update camera‑dependent calculations; prepare deck.gl layers
onAfterRender(context) Immediately following render completion Synchronize overlay positions; capture frame statistics

Plugins omitting any hook are handled gracefully—the PluginManager checks for method existence before invocation.

Complete Plugin Implementation Example

The following demonstrates a raster layer plugin exercising multiple lifecycle stages:

// packages/plugins/src/plugins/example-raster.ts
import type { Plugin, PluginContext } from "../types";

export const exampleRaster: Plugin = {
  id: "example-raster",
  
  onRegister({ logger }: PluginContext) {
    logger.info("example‑raster: registered");
    // Initialize plugin‑specific state
  },
  
  onActivate({ map, logger }: PluginContext) {
    logger.info("example‑raster: activated");
    
    map.addSource("example-raster", {
      type: "raster",
      tiles: ["https://example.com/tiles/{z}/{x}/{y}.png"],
      tileSize: 256,
    });
    
    map.addLayer({
      id: "example-raster-layer",
      type: "raster",
      source: "example-raster",
      paint: { "raster-opacity": 0.8 },
    });
  },
  
  onDeactivate({ map, logger }: PluginContext) {
    logger.info("example‑raster: deactivated");
    
    if (map.getLayer("example-raster-layer")) {
      map.removeLayer("example-raster-layer");
    }
    if (map.getSource("example-raster")) {
      map.removeSource("example-raster");
    }
  },
  
  onUpdate(prevConfig, newConfig, { map }: PluginContext) {
    if (prevConfig.opacity !== newConfig.opacity) {
      map.setPaintProperty(
        "example-raster-layer",
        "raster-opacity",
        newConfig.opacity
      );
    }
  },
  
  onUnload({ logger }: PluginContext) {
    logger.info("example‑raster: unloaded");
  },
};

This plugin:

  1. Logs registration via onRegister
  2. Creates map resources on onActivate
  3. Updates opacity reactively through onUpdate
  4. Cleans up layers and sources in onDeactivate
  5. Performs final logging in onUnload

PluginContext Object Structure

All lifecycle hooks receive identical context:

  • store: Zustand state management store for reactive data access
  • map: Active MapLibre GL JS map instance
  • api: GeoLibre‑specific utility methods for common operations
  • logger: Structured logging interface with severity levels

Summary

  • Registration path: Built‑in plugins export from packages/plugins/src/plugins/, aggregate in packages/plugins/src/index.ts as builtInPlugins, and register via usePlugins hook calling pluginManager.registerPlugin()
  • PluginManager location: Singleton in packages/plugins/src/plugin-manager.ts with methods for registration, activation, deactivation, and retrieval
  • Six lifecycle hooks: onRegister, onActivate, onDeactivate, onUnload, onUpdate, plus frame hooks onBeforeRender/onAfterRender
  • Hook parameter: All receive PluginContext with store, map, API, and logger
  • Graceful degradation: Unimplemented hooks are skipped; no runtime errors for missing methods

Frequently Asked Questions

How does GeoLibre distinguish between built‑in and external plugins?

Built‑in plugins are statically imported and bundled with the application through the builtInPlugins map, while external plugins are loaded dynamically at runtime. The PluginManager handles both identically after registration—no runtime distinction exists in the registry.

Can a plugin activate itself automatically on registration?

The PluginManager does not auto‑activate plugins during registration. Activation requires an explicit activatePlugin() call, typically triggered by user interaction in the Plugin panel or by persistent state restoration at application startup.

What happens if onActivate throws an error?

The PluginManager wraps hook invocations in try‑catch blocks. Errors are logged via the context logger, and the plugin state remains consistent—partial activation does not corrupt the registry or affect other plugins.

Are the render hooks onBeforeRender/onAfterRender limited to visual plugins?

These hooks are available to all plugins but incur performance costs when registered. Non‑visual plugins should omit them to avoid unnecessary per‑frame overhead. The PluginManager only attaches render listeners for plugins implementing these methods.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →