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:
- Logs registration via
onRegister - Creates map resources on
onActivate - Updates opacity reactively through
onUpdate - Cleans up layers and sources in
onDeactivate - Performs final logging in
onUnload
PluginContext Object Structure
All lifecycle hooks receive identical context:
store: Zustand state management store for reactive data accessmap: Active MapLibre GL JS map instanceapi: GeoLibre‑specific utility methods for common operationslogger: Structured logging interface with severity levels
Summary
- Registration path: Built‑in plugins export from
packages/plugins/src/plugins/, aggregate inpackages/plugins/src/index.tsasbuiltInPlugins, and register viausePluginshook callingpluginManager.registerPlugin() - PluginManager location: Singleton in
packages/plugins/src/plugin-manager.tswith methods for registration, activation, deactivation, and retrieval - Six lifecycle hooks:
onRegister,onActivate,onDeactivate,onUnload,onUpdate, plus frame hooksonBeforeRender/onAfterRender - Hook parameter: All receive
PluginContextwith 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →