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 app instance 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 += 1 invalidates 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 locations
  • ViewStateControl – map position/zoom/bearing
  • ColorbarGuiControl – legend colorbar configuration
  • LegendGuiControl – layer legend visibility and content
  • HtmlGuiControl – 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:

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 →