How to Load External Plugins from ZIP Files and plugin.json Manifests in GeoLibre
GeoLibre loads external plugins through a three-phase pipeline—discovery, validation, and activation—supporting ZIP archives, unpacked directories, and remote HTTPS URLs pointing to plugin.json manifests.
GeoLibre's plugin system treats external extensions as self-contained bundles that can be distributed as ZIP files, loaded from local directories, or fetched directly from HTTPS endpoints. This article explains the complete loading pipeline implemented in the opengeos/GeoLibre desktop client, including the specific files and validation rules that govern how external plugins are discovered and activated.
Plugin Bundle Formats
GeoLibre accepts external plugins in three distinct formats:
- ZIP archives containing a
plugin.jsonmanifest at the root - Unpacked directories with the same structure, typically used during plugin development
- Remote HTTPS URLs pointing directly to a
plugin.jsonfile
Each format follows identical validation rules once the manifest is located.
Discovery Phase: Locating plugin.json
The discovery process varies by source type, with dedicated handlers for each format.
ZIP Archive Discovery
When users install a plugin via Manage Plugins → Settings → Install from file, the file is passed to plugin-archive-unpack.ts. The helper function locatePluginJsonInArchive scans ZIP entries and returns the path of the first plugin.json found, preferring root manifests and ignoring macOS __MACOSX/ folders. Per the source code at lines 76-100, the scanner handles nested directories by selecting the shallowest valid manifest.
Local Directory Discovery
For development copies or manually installed plugins, bundled-plugins.ts reads plugins/<dir>/plugin.json directly from the filesystem (lines 21-31). This path is used for bundled drop-ins that ship with the application.
Remote URL Discovery
Remote manifests are stored in the project's manifestUrls array (documented in project-format.md). When the app starts or a user adds a URL, loadExternalPluginFromUrl fetches the JSON file. The implementation at lines 278-284 of external-plugins.ts handles the network request and initial parsing.
Validation Phase: Security and Integrity Checks
After discovery, GeoLibre enforces strict validation before accepting any plugin. The following checks are performed in sequence:
| Check | Requirement | Implementation Location |
|---|---|---|
| JSON syntax | Valid parseable JSON | plugin-archive-unpack.ts lines 122-124 |
| Required fields | id, name, version, and entry must be present; manifestUrl required for remote plugins |
external-plugins.ts lines 421-428 |
| Entry file existence | The entry path must resolve to an existing .js or .mjs bundle relative to the manifest |
external-plugins.ts (throws mismatch error if check fails) |
| Size limits | Archive ≤ 50 MiB; individual assets ≤ 2 GiB | remote-file-formats.ts and install-time enforcement |
| Signature verification | Tauri builds verify exactly one plugin.json exists and is readable |
src-tauri/src/lib.rs lines 1354-1367 |
Validation failures abort the installation and surface an error dialog to the user. The native Rust validation in lib.rs provides an additional security layer for desktop builds.
Activation Phase: Runtime Loading
Once validation passes, plugins are activated through dynamic module import.
Bundled vs. Runtime Activation
- Bundled drop-ins: May specify
"activeByDefault": truein their manifest to appear on startup (documented inplugin-api.mdline 832) - Runtime plugins: Loaded on demand; the
entrymodule is dynamicallyimport()-ed and the exported object is inspected
The activation code in external-plugins.ts (lines 421-427) performs version/id/name sanity checks, ensures the exported object implements the GeoLibrePlugin interface, and registers the plugin with the internal PluginRegistry. Once registered, UI controls appear in ManagePluginsDialog.tsx.
Persistence and Storage
Plugin persistence differs between platforms:
- Desktop: Installed plugin folders or downloaded ZIPs are stored under
~/.config/GeoLibre/plugins/. The registry persists plugin IDs for automatic reloading on subsequent launches. - Web: Remote manifests are refetched each session. Only the manifest URL is persisted in the project file, as browsers cannot write to the filesystem.
Creating Compatible Plugin Bundles
To build a plugin that GeoLibre can load, structure your ZIP with this layout:
my-plugin/
├── plugin.json # manifest (must be at root)
├── dist/
│ └── my-plugin.js # entry bundle (export default GeoLibrePlugin)
└── style/
└── my-plugin.css # optional stylesheet
The entry field in plugin.json must point to the JavaScript bundle relative to the manifest location.
Code Examples
Minimal plugin.json Manifest
{
"id": "demo-plugin",
"name": "Demo Plugin",
"version": "0.1.0",
"entry": "dist/demo-plugin.js",
"manifestUrl": "https://example.com/demo-plugin/plugin.json",
"minGeoLibreVersion": "2.0.0"
}
The manifest must be located at the ZIP root or the shallowest */plugin.json path in the archive.
Installing a ZIP from the UI
- Open Manage Plugins → Settings
- Click Install from file and select your
.zipfile - GeoLibre runs the full validation pipeline; successful plugins appear in the active list
Programmatic Loading via Internal API
import { loadExternalPluginFromUrl } from '@geolibre/plugins';
async function addRemotePlugin(url: string) {
try {
const plugin = await loadExternalPluginFromUrl(url);
console.log(`✅ Loaded ${plugin.id} v${plugin.version}`);
} catch (e) {
console.error('❌ Plugin failed to load:', e);
}
}
// Example usage
addRemotePlugin('https://geolibre.app/plugins/sample/plugin.json');
This function fetches, validates, and registers the plugin in a single call.
Core Implementation Files
| File | Purpose |
|---|---|
apps/geolibre-desktop/src/lib/plugin-archive-unpack.ts |
ZIP scanning and plugin.json location |
apps/geolibre-desktop/src/lib/external-plugins.ts |
Manifest fetching, validation, and activation |
apps/geolibre-desktop/vite-plugins/bundled-plugins.ts |
Build-time bundled plugin loading |
apps/geolibre-desktop/src/components/layout/ManagePluginsDialog.tsx |
Plugin management UI |
apps/geolibre-desktop/src-tauri/src/lib.rs |
Native Tauri validation layer |
docs/plugin-api.md |
Public plugin.json specification |
Summary
- GeoLibre's three-phase pipeline—discovery, validation, activation—ensures only safe, well-formed plugins load
- ZIP files are scanned by
locatePluginJsonInArchivewith__MACOSX/filtering and nested directory handling - Remote URLs are fetched via
loadExternalPluginFromUrland cached by manifest URL - Validation enforces required fields, entry file existence, size limits (50 MiB archives, 2 GiB assets), and native signature checks on Tauri
- Activation uses dynamic
import()of the entry module with interface compliance verification
Frequently Asked Questions
What happens if my ZIP has multiple plugin.json files?
GeoLibre selects the shallowest plugin.json in the archive hierarchy, preferring root-level manifests. Nested manifests in subdirectories are ignored unless no root-level manifest exists. The __MACOSX/ folder is explicitly excluded from scanning.
Can I load plugins from HTTP instead of HTTPS?
No. Remote plugin manifests must use HTTPS URLs. This restriction is enforced by the URL validation logic and aligns with GeoLibre's security model for external code execution.
How do I update a plugin without reinstalling?
For remote plugins, increment the version field in your hosted plugin.json. GeoLibre re-fetches remote manifests each session and will detect version changes. For ZIP-installed plugins, users must reinstall the updated archive through Manage Plugins → Settings → Install from file.
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 →