Asset Management
How plugins load and serve static assets (images, icons, data files), and how the v2 document asset store relates to plugin resources.
Two Kinds of "Assets"
| Kind | Stored in | Accessed via | Lifetime |
|---|---|---|---|
| Plugin resources (shipped files) | Plugin directory | usePluginResource(path) |
Installed with the plugin |
| Document assets (user content) | Content-addressed asset store | AssetService (ctx.assets) |
Tied to the open document |
Plugin resources are static files you ship inside your plugin ZIP. Document assets are runtime content — layers, bakes, imported files — managed by the engine's sharded store. Do not confuse the two APIs.
usePluginResource Hook
import { usePluginResource } from '@opengpex/editor/core/context';
function MyPanel() {
const logoUrl = usePluginResource('visuals/logo.svg');
const presetsUrl = usePluginResource('data/presets.json');
return <img src={logoUrl} alt="Logo" />;
}
How It Works
The hook resolves to /api/plugins/serve/${_folderName}/${relativePath}. The serve API searches in order:
- Dynamic data directory:
data/plugins/user/${folderName}/${path} - Static source directory:
src/lib/opengpex/plugins/user/${folderName}/${path}
This dual fallback means identical code works in development (Track 1) and production (Track 2) with zero configuration. The same proxy also serves the plugin's compiled bundle itself (dist/index.js) to the dynamic loader.
Supported Asset Types
The serve API infers MIME types for these formats (anything else falls back to application/octet-stream):
| Type | Extensions | Use Case |
|---|---|---|
| Code/Styles | .js, .mjs, .css | The plugin bundle itself, stylesheets |
| Images | .png, .jpg, .jpeg, .gif, .webp, .avif, .svg, .ico | Icons, thumbnails, overlays |
| Data | .json | Configuration, presets |
| Fonts | .woff2, .woff, .ttf, .otf | Custom typography |
| Documents | Help docs | |
| Media | .mp3, .mp4 | Sound/video (rare) |
| Binary | .wasm | WASM modules |
Directory Naming
Asset directories may use any name — the system does not restrict it:
MyPlugin/
├── index.tsx
├── components.tsx
├── visuals/ # any name works
│ └── logo.svg
├── icons/
│ └── tool.png
└── data/
└── presets.json
All subdirectories are automatically collected by pnpm pack-plugin.
Document Assets via AssetService (v2)
When your plugin produces or consumes document content, use the content-addressed store:
execute: async (ctx) => {
// Import: registers a page and its source file, deriving IDs from the content hash
const record = await ctx.assets.storeBundle(page, sourceBlob);
// Display: live blob URL for <img> / <canvas> consumers
const url = ctx.assets.getURL(record.id);
// Decoded high-depth pixels for analysis (Uint16Array | Float32Array, or null)
const pixels = await ctx.assets.getDec(record.id);
// Original encoded file (follows fileHashOf)
const file = await ctx.assets.getRaw(record.id);
}
Key points:
- Asset IDs are hash-derived:
${hash}#Nfor multi-page imports, bare${hash}for bake results,raw:${hash}for original sources. assets.sweep(activeIds)performs garbage collection; the engine calls it on frame deletion and periodically. Do not hold long-lived references toblob:URLs of swept assets.- Zero-sized bitmaps are rejected — the store skips and logs rather than feeding invalid textures to WebGPU.
Security
- Path traversal (
../) in resource paths is blocked by the serve API - Plugins can only read files inside their own directory
- No access to host system files or other plugins' resources
Next Steps
- API Reference — full AssetService table
- Packaging & Distribution — how resources land in the ZIP
- Your First Plugin — end-to-end tutorial
Last updated: 2026-10-12