# How Dynamic Plugin Loading from ZIP Files Works in GeoLibre

> Learn how GeoLibre dynamically loads plugins from ZIP files in the browser. Explore fflate decompression, manifest validation, and Object URL execution for efficient plugin management.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: internals
- Published: 2026-07-29

---

**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`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/tauri-io.ts) to the lifecycle management in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest and a [`plugin.js`](https://github.com/opengeos/GeoLibre/blob/main/plugin.js) entry 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`](https://github.com/opengeos/GeoLibre/blob/main/tests/plugin-archive-unpack.test.ts)) transforms validated ZIP contents into a **PluginBundle** object. This bundle encapsulates:

- The original `archiveName` for 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.

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/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.

```typescript
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:

```typescript
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.

```typescript
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 `bundleFromZipBytes` function validates archives for [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifests and safe file paths before creating executable **PluginBundle** objects with dynamically generated Object URLs.
- **PluginManager** in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) handles registration, activation with scoped APIs via `scopeAppToPlugin`, and automatic rollback on failed initialization.
- Plugin state persists via `ProjectPluginState.manifestUrls` and restores automatically on application startup, supporting both local files and remote URLs.
- `PluginManager.unregister` ensures 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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest and a [`plugin.js`](https://github.com/opengeos/GeoLibre/blob/main/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.