Dynamic plugin loader
Install ES-module plugins from URL (source, stream, engine, dataset, component), persist, restore, safety rules.
This page
Dynamic plugin loader
Abstract
The loader (src/plugins/loader.ts) lets users install third-party or example plugins at runtime via dynamic import(). Supported kinds: source, stream, engine, dataset, component (e.g. PYNE Agent). Installed URLs persist in localStorage under:
| Key | Role |
|---|---|
pynescript.axis.plugins.v1 | Installed plugin list |
On boot, restoreInstalledPlugins() re-imports every saved URL.
Conceptual model
Rendering…
Interface surface
loadPluginFromUrl(url: string): Promise<InstalledPlugin>
removePlugin(id: string, kind?: string): void
getInstalledPlugins(): InstalledPlugin[]
restoreInstalledPlugins(): Promise<void>
normalizePluginUrl(url: string): string
assertSafePluginUrl(href: string): void
PLUGINS_KEY // 'pynescript.axis.plugins.v1'
InstalledPlugin
{ url, id, name, kind, description? }
Module export shapes accepted
export default pluginexport const plugin = …- Module namespace object that is the plugin
Must include id and kind. Required methods / contracts:
| kind | Requirement |
|---|---|
source | fetchHistorical |
stream | start |
engine | run |
dataset | dataset fetch contract (onchain catalog) |
component | ComponentPlugin — slots / mount; optional init / dispose (registered on the unified registry) |
storage | Rejected via URL install — use built-in local/cloud/git |
Internals
URL normalization
// /src/plugins/foo.js → /plugins/foo.js
href.replace(/(^|\/)src\/plugins\//, '$1plugins/');
Dev-only Vite paths never ship in dist/; production examples live under public/plugins/ → /plugins/….
Safety
assertSafePluginUrl rejects:
javascript:vbscript:data:text/html…
This is not a full sandbox. Dynamic plugins run with the page’s privileges (network, DOM). Treat plugin URLs like executable code supply chain.
Dedup on install
List filters out same URL or same (kind, id) before append — reloading an updated module replaces the entry.
Unregister
removePlugin calls unregisterDynamic* for the resolved kind (or all kinds if unknown) and rewrites the installed list.
Bootstrap coupling
loadPluginFromUrl / restoreInstalledPlugins call ensureBuiltins() first so catalogs exist before dynamic ids land.
Manager UX (product)
From src/plugins/README.md:
- Manager → Plugins → Load from URL
- Example:
http://localhost:8081/plugins/example-coingecko-source.js - Export installed / Import… for machine migration
- Auto-activate patterns for source/stream/engine after install (Manager catalog Use)
Shipped examples (also under public/plugins/):
| File | Kind |
|---|---|
example-coingecko-source.js | source |
example-tiny-pyne-engine.js | engine |
example-cf-do-stream.js | stream |
External component example: PYNE Agent —
https://pyne-agent-worker.cryptolinx.workers.dev/plugin/axis-pine-agent.js
Invariants & edge cases
- CORS on module URL — the JS file origin must allow module import (same-origin is easiest).
- CORS on plugin’s own API — separate problem; may need proxy (see plugins README).
- Partial restore — one bad URL logs error and continues others.
- No code signing — trust the URL owner.
Worked example
# After bun run build + axis_pwa_server.py
# Load:
http://127.0.0.1:8081/plugins/example-coingecko-source.js
Then select CoinGecko in the source picker (id coingecko).
Failure modes
| Error | Meaning |
|---|---|
URL required | Empty input |
Plugin URL scheme not allowed | Dangerous scheme |
Module did not export a plugin object | Wrong export shape |
Plugin needs id and kind | Incomplete object |
Source plugin needs fetchHistorical() | Incomplete source |
Unknown plugin kind | Typo or unsupported kind (not in source/stream/engine/dataset/component) |
| Restore log errors | 404, network, syntax error in remote JS |