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:

KeyRole
pynescript.axis.plugins.v1Installed plugin list
Studio Plugins Install tab: ES module URL field and same-origin examples

On boot, restoreInstalledPlugins() re-imports every saved URL.

Conceptual model

Diagram

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

  1. export default plugin
  2. export const plugin = …
  3. Module namespace object that is the plugin

Must include id and kind. Required methods / contracts:

kindRequirement
sourcefetchHistorical
streamstart
enginerun
datasetdataset fetch contract (onchain catalog)
componentComponentPluginslots / mount; optional init / dispose (registered on the unified registry)
storageRejected 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:

  1. Manager → Plugins → Load from URL
  2. Example: http://localhost:8081/plugins/example-coingecko-source.js
  3. Export installed / Import… for machine migration
  4. Auto-activate patterns for source/stream/engine after install (Manager catalog Use)

Shipped examples (also under public/plugins/):

FileKind
example-coingecko-source.jssource
example-tiny-pyne-engine.jsengine
example-cf-do-stream.jsstream

External component example: PYNE Agent
https://pyne-agent-worker.cryptolinx.workers.dev/plugin/axis-pine-agent.js

Invariants & edge cases

  1. CORS on module URL — the JS file origin must allow module import (same-origin is easiest).
  2. CORS on plugin’s own API — separate problem; may need proxy (see plugins README).
  3. Partial restore — one bad URL logs error and continues others.
  4. 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

ErrorMeaning
URL requiredEmpty input
Plugin URL scheme not allowedDangerous scheme
Module did not export a plugin objectWrong export shape
Plugin needs id and kindIncomplete object
Source plugin needs fetchHistorical()Incomplete source
Unknown plugin kindTypo or unsupported kind (not in source/stream/engine/dataset/component)
Restore log errors404, network, syntax error in remote JS

See also