GeoLibre Components Plugin Architecture: How It Wraps maplibre-gl-components Controls
The GeoLibre Components plugin uses a three-layer architecture—dynamic loader, constructor cache, and control manager—to lazily import, memoize, and lifecycle-manage UI controls from the maplibre-gl-components library.
The Components plugin in GeoLibre serves as the integration bridge between the framework's store-driven architecture and the rich set of UI widgets provided by maplibre-gl-components. Located at packages/plugins/src/plugins/maplibre-components.ts, this plugin implements a sophisticated lazy-loading system that keeps initial bundle sizes small while providing resilient error handling and full state persistence capabilities.
Three-Layer Architecture Overview
The maplibre-gl-components wrapper architecture separates concerns into distinct layers:
| Layer | Responsibility | Key Mechanism |
|---|---|---|
| Dynamic loader | Runtime import of external packages | defaultLoadComponentsModules() with Promise.all([import(...)]) |
| Constructor cache | Single-import guarantee across session | componentsConstructorsPromise memoisation |
| Control manager | Instantiation, mounting, lifecycle, state | createComponentsControl(), mountComponentsControl(), activate/deactivate |
Dynamic Loading and Chunk Resilience
The plugin defers loading maplibre-gl-components until activation, using dynamic import() expressions. This enables code-splitting and protects against stale deployment artifacts.
// packages/plugins/src/plugins/maplibre-components.ts (lines 38-46)
const defaultLoadComponentsModules = (): Promise<ComponentsModules> =>
Promise.all([
import("maplibre-gl-components"),
import("maplibre-gl-splat").catch(() => null), // graceful fallback
]);
The optional splat package fails silently to null, preventing total UI failure when a chunk is unavailable. Failed primary imports throw a user-facing error with clear recovery instructions rather than cryptic module loader exceptions.
Constructor Memoisation Pattern
The getComponentsConstructors() function implements a singleton promise pattern that guarantees exactly one network request per session while allowing retry after transient failures:
// packages/plugins/src/plugins/maplibre-components.ts (lines 64-73)
export const getComponentsConstructors = (): Promise<ComponentsConstructors> => {
componentsConstructorsPromise ??= loadComponentsModules()
.then(([components, splat]) => {
if (!components) {
throw new Error(
"The map controls could not be loaded … reload the page."
);
}
const { AddVectorControl, BookmarkControl, … } = components;
// Prefer dedicated splat exports, fall back to bundled copy
const GaussianSplatControlClass = (splat?.GaussianSplatControl ??
components.GaussianSplatControl) as GaussianSplatControlConstructor;
…
return { AddVectorControl, BookmarkControl, … };
})
.catch((e) => {
componentsConstructorsPromise = null; // never cache failures
throw e;
});
return componentsConstructorsPromise;
};
Critical implementation detail: The .catch() block resets componentsConstructorsPromise to null, ensuring that a failed load can be retried on the next activation attempt rather than returning the rejected promise indefinitely.
Control Creation and GeoLibre Integration
Each control receives typed options objects (e.g., ADD_VECTOR_OPTIONS, COG_RASTER_OPTIONS, PMTILES_OPTIONS) that merge upstream defaults with GeoLibre-specific configurations:
- CSS class prefixing: all controls receive
"geolibre-"prefixed class names - Position mapping: grid and individual control positions derived from
componentsControlPosition,cogRasterControlPosition, etc. - Store access: the
appinstance passed through options enables callbacks into GeoLibre's layer management and dialog systems
The ControlGrid container is instantiated via createComponentsControl() at lines 146-150:
const createComponentsControl = async (app: GeoLibreAppAPI): Promise<ControlGrid | null> => {
const { ControlGrid: ControlGridClass } = await getComponentsConstructors();
if (!pluginActive) return null;
return new ControlGridClass(getComponentsOptions(app));
};
Mounting and Lifecycle Management
Activation triggers async construction and map attachment:
activate: (app) => {
pluginActive = true;
if (componentsControl) return mountComponentsControl(app);
createAndMountComponentsControl(app);
},
Deactivation performs coordinated cleanup:
- Revision bump:
componentsControlRevision += 1invalidates stale control references - Teardown cascade: individual
teardown*helpers remove each control from the map and detach listeners - State preservation: control states are extracted before destruction for subsequent restoration
The full deactivation logic spans lines 177-187 and 186-199, handling edge cases like partially-initialized grids and interdependent control lifecycles.
State Persistence for Panel Controls
Five controls implement serializable state interfaces for project save/restore:
BookmarkControl– saved view locationsViewStateControl– map position/zoom/bearingColorbarGuiControl– legend colorbar configurationLegendGuiControl– layer legend visibility and contentHtmlGuiControl– custom HTML overlay content
The plugin attaches setState methods to these control instances (lines 292-305), which are invoked from store-derived restoration logic such as restoreVisibleLayers (lines 61-77).
Test Harness and Injectable Loaders
The architecture supports dependency injection for testing through __setComponentsModuleLoaderForTests:
import { __setComponentsModuleLoaderForTests } from "@geolibre/plugins";
__setComponentsModuleLoaderForTests(() => Promise.reject(new Error("forced error")));
await getComponentsConstructors(); // will reject
__setComponentsModuleLoaderForTests(null); // restore real loader
The dedicated test file tests/components-constructors-loader.test.ts validates error recovery, ensuring that preload failures result in clean fallback behavior rather than poisoned singleton state.
Typical Integration Patterns
Basic plugin activation
import { maplibreComponentsPlugin } from "@geolibre/plugins";
app.addPlugin(maplibreComponentsPlugin);
Translating control labels
import { setBookmarkLabels } from "@geolibre/plugins";
setBookmarkLabels({
captureStateLabel: "Inclure l'état complet du calque",
exportLabel: "Exporter",
});
The setBookmarkLabels function (lines 35-42) mutates an internal bookmarkLabels object that BookmarkControl reads at instantiation time.
Supporting Infrastructure
| File | Purpose |
|---|---|
packages/plugins/src/plugins/map-projection-utils.ts |
Enforces Mercator projection before component loading |
packages/plugins/src/plugins/shared-deck-overlay.ts |
Shared Deck.gl overlay for STAC search, Gaussian splat controls |
packages/plugins/src/plugins/internal-layers.ts |
Excludes helper layers from ControlGrid UI enumeration |
Summary
- Three-layer architecture: dynamic loader → constructor cache → control manager separates network, memoisation, and DOM concerns
- Resilient imports: optional packages fail gracefully; primary failures are recoverable and user-informative
- Singleton promise pattern: guarantees single network request with retry-after-failure semantics
- Full lifecycle management: activation, deactivation, and state persistence integrated with GeoLibre's store
- Testable design: injectable loaders and resettable promise state enable comprehensive unit testing
Frequently Asked Questions
How does the Components plugin avoid bundling maplibre-gl-components in the main application?
The plugin uses dynamic import() expressions in defaultLoadComponentsModules() rather than static ES module imports. This causes bundlers to split the dependency into a separate chunk that is only fetched when activate() runs, keeping the initial GeoLibre bundle minimal.
What happens if maplibre-gl-components fails to load due to a network error or stale deployment?
The getComponentsConstructors() catch block resets componentsConstructorsPromise to null and throws a descriptive error message. Users see actionable text ("reload the page") rather than technical module loader failures, and the next plugin activation attempt triggers a fresh import rather than returning the cached rejection.
Can I use the Components plugin with a custom build of maplibre-gl-components?
Yes. The plugin exposes __setComponentsModuleLoaderForTests() which can be repurposed to inject any compatible module loader. Replace the default defaultLoadComponentsModules with a function returning your custom package exports, maintaining the [ComponentsModules, SplatModule | null] tuple contract.
Which controls support state persistence across project saves?
The plugin implements state interfaces for BookmarkControl, ViewStateControl, ColorbarGuiControl, LegendGuiControl, and HtmlGuiControl. Each receives a setState method attachment that GeoLibre's store invokes during project restoration workflows.
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 →