How GeoLibre Distinguishes Built-In vs External Plugins
GeoLibre distinguishes built-in plugins from external plugins through separate discovery mechanisms: built-ins are statically imported at compile time from packages/plugins/src/plugins/, while external plugins are dynamically discovered at runtime from apps/geolibre-desktop/public/plugins/ with validation via plugin.json manifests.
In the GeoLibre open-source geospatial platform, the plugin system implements a dual-path architecture that seamlessly integrates core functionality with user-extensible features. This article examines how the source code differentiates between built-in plugins (shipped with the application) and external plugins (user-installed), covering their discovery, registration, loading, and security models.
Discovery Mechanisms: Static vs Dynamic
The first distinction appears in how each plugin type is found. Built-in and external plugins follow fundamentally different discovery paths.
Built-In Plugin Discovery
Built-in plugins reside in packages/plugins/src/plugins/ as TypeScript modules. The registerBuiltInPlugins() function in packages/plugins/src/plugins/index.ts uses a static import pattern to collect all plugins at build time:
// packages/plugins/src/plugins/index.ts
import { registerPlugin } from '@geolibre/core';
import { ExamplePlugin } from './example-plugin';
// All built‑in plugins are imported statically.
registerPlugin(ExamplePlugin);
This approach leverages ES module static analysis, allowing bundlers like Vite to tree-shake and optimize the final bundle.
External Plugin Discovery
External plugins use a dynamic discovery process via the usePlugins hook in apps/geolibre-desktop/src/hooks/usePlugins.ts. This hook scans the public/plugins/ directory, parses each plugin.json manifest, and constructs runtime PluginDefinition objects:
// apps/geolibre-desktop/src/hooks/usePlugins.ts
async function loadExternalPlugins() {
const pluginManifests = await fetch('/plugins/').then(r => r.json());
for (const manifest of pluginManifests) {
const moduleUrl = `/plugins/${manifest.id}/${manifest.main}`;
const pluginModule = await import(/* @vite-ignore */ moduleUrl);
registerPlugin(pluginModule.default);
}
}
Runtime discovery enables users to install plugins without rebuilding the application.
Registration Timing: Compile-Time vs Runtime
The registration timing fundamentally separates the two plugin types.
| Aspect | Built-In Plugins | External Plugins |
|---|---|---|
| When | Once at application startup | Dynamically when user adds plugin |
| Trigger | Monorepo build process | POST /plugins/add API call or app startup |
| Validation | TypeScript compiler | Runtime plugin.json schema validation |
For built-in plugins, registration happens implicitly through static imports. External plugins require explicit registration through loadExternalPlugins(), which validates required manifest fields before calling pluginsRegistry.register(externalDef).
Loading Strategies: Bundled vs Lazy-Loaded
Loading behavior differs significantly between built-in and external plugins in GeoLibre.
-
Built-in plugins are bundled with the application. Build tools include their code in the main JavaScript bundle, eliminating additional HTTP requests.
-
External plugins are lazy-loaded on demand. When a layer using an external plugin activates, the system fetches the JavaScript entry point specified in
plugin.json.main, evaluates it via dynamicimport(), then registers the exported object.
This lazy-loading pattern prevents external plugins from impacting initial application load time.
Security Model: Trusted vs Sandboxed
Security represents the most critical distinction. Built-in and external plugins operate under different trust boundaries.
Built-in plugins execute with full application privileges. As core project code maintained in the opengeos/GeoLibre repository, they undergo code review and are trusted by default.
External plugins are sandboxed through multiple mechanisms:
- Same-origin policy: Served from
public/plugins/, isolated from core codebase - Manifest trust flag:
plugin.jsondeclares atrustedboolean - Capability restrictions: Untrusted plugins lose filesystem access and other sensitive operations
// Conceptual check within plugin execution context
if (!plugin.manifest.trusted) {
disableFilesystemAccess();
restrictNetworkRequests();
}
Unified Registry: The Abstraction Layer
Despite divergent paths, both plugin types converge at packages/core/src/pluginsRegistry.ts. This central registry provides a unified interface:
// Any part of the UI can request a plugin by id:
import { pluginsRegistry } from '@geolibre/core';
const myPlugin = pluginsRegistry.get('my-plugin-id');
myPlugin?.run(); // Works whether the plugin is built‑in or external.
The registry's getAll() method returns mixed collections, allowing UI components to remain origin-agnostic. This design pattern—separate discovery, unified consumption—enables GeoLibre's flexible extensibility without complicating downstream code.
Upgrade and Version Management
Each plugin type follows distinct versioning workflows.
-
Built-in plugins: Version tied to
packages/plugins/package.json. Updates require source modification and workspace rebuild (npm run build). -
External plugins: Self-contained versioning in
plugin.json. The UI provides "Check for updates" functionality that re-downloads manifests and refreshes registration without application restart.
Summary
-
Built-in plugins live in
packages/plugins/src/plugins/, discovered via static imports at compile time, bundled with the application, and run with full trust. -
External plugins reside in
apps/geolibre-desktop/public/plugins/, discovered dynamically viausePlugins, lazy-loaded on activation, and sandboxed by origin and manifest flags. -
Both types register through
pluginsRegistry.register()inpackages/core/src/pluginsRegistry.ts, providing a unified consumption interface. -
The
plugin.jsonmanifest drives external plugin metadata, validation, and trust determination.
Frequently Asked Questions
What file defines the structure for external plugin manifests?
The plugin.json file in each external plugin directory defines the structure. It specifies the plugin ID, version, entry point (main), and trust status. GeoLibre validates this manifest at discovery time before registration.
Can external plugins access the same APIs as built-in plugins?
External plugins access the same plugin registry APIs but face runtime restrictions. The trusted flag in plugin.json determines capability grants—untrusted plugins lose filesystem and certain network privileges despite sharing the registration interface.
How does GeoLibre prevent malicious external plugins from harming the system?
Multiple layers protect the system: browser same-origin policy isolates plugin code; the public/plugins/ directory structure prevents path traversal; manifest validation rejects malformed plugins; and the trust flag disables sensitive capabilities for unverified sources. Review apps/geolibre-desktop/src/hooks/usePlugins.ts for implementation details.
Where should I place a new built-in plugin during development?
Create your TypeScript module under packages/plugins/src/plugins/, then import and register it in packages/plugins/src/plugins/index.ts. The static import system will automatically include it in the next build.
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 →