How Dynamic Plugin Loading from ZIP Files Works in GeoLibre
GeoLibre decompresses ZIP archives entirely in the browser using the fflate library, validates the contents against a manifest schema, and executes plugin code via dynamically generated Object URLs while managing lifecycle events through a centralized PluginManager.
GeoLibre supports extensible GIS workflows through client-side dynamic plugin loading from ZIP archives, allowing users to install extensions without server-side processing or native installers. The architecture leverages the lightweight fflate library for decompression and a rigorous validation pipeline to ensure code safety before execution. This guide examines the implementation details, from the initial byte extraction in apps/geolibre-desktop/src/lib/tauri-io.ts to the lifecycle management in packages/plugins/src/plugin-manager.ts.
Client-Side Decompression with fflate
GeoLibre performs all ZIP processing locally using the fflate library (import { unzip } from "fflate"), ensuring that sensitive plugin code never leaves the user's machine. The unzipArchive function in apps/geolibre-desktop/src/lib/tauri-io.ts handles the extraction, yielding a mapping of file paths to their corresponding Uint8Array contents.
When a user selects a local .zip file or provides a remote URL, the application reads the raw bytes into memory and passes them to the extraction layer. This approach eliminates network latency and server-side storage requirements while maintaining full control over the execution environment.
Archive Validation and Security
Before creating executable objects, GeoLibre validates the archive structure to prevent malicious payloads. The extraction logic explicitly checks for:
- Required files: Every valid plugin must contain a
plugin.jsonmanifest and aplugin.jsentry point. - Path traversal: The validator rejects any entries containing
../sequences to prevent directory escape attacks. - Metadata filtering: Hidden macOS directories such as
__MACOSX/are automatically stripped from the extraction results.
These checks occur during the unpacking phase in bundleFromZipBytes, ensuring that only sanitized file contents reach the execution engine.
Constructing Plugin Bundles
The bundleFromZipBytes function (demonstrated in tests/plugin-archive-unpack.test.ts) transforms validated ZIP contents into a PluginBundle object. This bundle encapsulates:
- The original
archiveNamefor identification - Raw byte arrays for each extracted file
- A dynamically generated Object URL created via
URL.createObjectURL(new Blob([code]))
Using Object URLs allows GeoLibre to treat the plugin's JavaScript code as a standard ES module without writing to the file system. The bundle defers actual code evaluation until the plugin is activated, reducing initial load times and memory consumption.
// UI handler for the “Choose .zip” button
async function onZipSelected(file: File) {
const bytes = new Uint8Array(await file.arrayBuffer());
const bundle = await bundleFromZipBytes(file.name, bytes); // ← creates a PluginBundle
pluginManager.register(bundle.plugin); // ← registers it
}
Registration and Activation Lifecycle
Once bundled, plugins enter the management layer controlled by PluginManager (packages/plugins/src/plugin-manager.ts). The register method adds the plugin to an internal plugins map, using the manifest's id field as the unique key.
Registration makes the plugin's declared assets—toolbar menus, map controls, and settings—available to the application shell. However, the code remains inert until explicitly activated.
Scoped API Injection
Activation occurs through PluginManager.activate, which receives the target plugin ID and a scoped application API. The scopeAppToPlugin utility tags all UI elements and API calls with the plugin's identifier, ensuring proper ownership boundaries and preventing cross-plugin interference.
If activation involves asynchronous operations (such as dynamic imports within the plugin code), the manager tracks the resulting promise. Should the promise reject or resolve to false, the system automatically rolls back the activation state, maintaining application stability.
if (!pluginManager.isActive("my-plugin")) {
await pluginManager.activate("my-plugin", appApi);
}
State Persistence Across Sessions
GeoLibre maintains plugin installations across application restarts via ProjectPluginState.manifestUrls. When a project saves, PluginManager.getProjectState() serializes the list of installed ZIP locations (local paths or remote URLs) and active plugin IDs.
During startup, the restoration process fetches remote archives if necessary, re-registers each bundle, and re-activates previously active plugins:
const saved = await loadProject(); // reads ProjectPluginState from storage
await Promise.all(
saved.manifestUrls.map(async url => {
const zipBytes = await fetch(url).then(r => r.arrayBuffer());
const bundle = await bundleFromZipBytes(url, new Uint8Array(zipBytes));
pluginManager.register(bundle.plugin);
})
);
pluginManager.restoreProjectState(saved, appApi);
Cleanup and Uninstallation
Removing a plugin requires more than simple deregistration. The PluginManager.unregister method deactivates the plugin if currently active, removes its entry from the internal maps, and clears any pending activation promises. This comprehensive cleanup prevents memory leaks and ensures that orphaned event listeners or map layers do not persist after removal.
const state = pluginManager.getProjectState(); // includes manifestUrls, active ids, etc.
await saveProject(state); // your own persistence logic
Summary
- GeoLibre uses fflate to decompress ZIP files entirely in the browser, eliminating server uploads and maintaining privacy.
- The
bundleFromZipBytesfunction validates archives forplugin.jsonmanifests and safe file paths before creating executable PluginBundle objects with dynamically generated Object URLs. - PluginManager in
packages/plugins/src/plugin-manager.tshandles registration, activation with scoped APIs viascopeAppToPlugin, and automatic rollback on failed initialization. - Plugin state persists via
ProjectPluginState.manifestUrlsand restores automatically on application startup, supporting both local files and remote URLs. PluginManager.unregisterensures clean removal by deactivating plugins, clearing promises, and preventing memory leaks.
Frequently Asked Questions
What library does GeoLibre use to decompress ZIP files in the browser?
GeoLibre uses the fflate library (import { unzip } from "fflate") to perform client-side decompression without sending data to a server. This implementation resides in the unzipArchive function within apps/geolibre-desktop/src/lib/tauri-io.ts, enabling completely local plugin installation workflows.
How does GeoLibre validate plugin ZIP archives before execution?
The system validates that the archive contains a plugin.json manifest and a plugin.js entry point, while explicitly blocking path traversal attacks (strings containing ../) and filtering out hidden macOS metadata directories (__MACOSX/). These security checks occur during the bundleFromZipBytes extraction process defined in the test suites and utilized by the production loader.
Can plugins be loaded from remote URLs instead of local files?
Yes, GeoLibre supports both local file uploads and remote URLs through the manifestUrls array in ProjectPluginState. During the restoration process, the application fetches remote ZIP bytes, reconstructs the PluginBundle, and registers the plugin without requiring the user to manually re-upload files.
What happens if a plugin fails to activate?
If a plugin's activation promise resolves to false or rejects due to runtime errors, the PluginManager automatically rolls back the activation state and removes the plugin from the active set. This prevents faulty plugins from crashing the GIS workspace or leaving the application in an inconsistent state.
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 →