UI Micro-frontend
The plugin's ui/ directory is a self-contained micro-frontend (MF) that the host serves as static assets under /plugin-runtime/{id}/** and loads into a sandboxed iframe under a strict Content Security Policy. Inside the iframe, the @infinia/plugin-sdk package provides a FengYuClient that bridges to the host over postMessage. The plugin never talks to the OS directly — every privileged action goes through the client.
The default entry: a Vue/Vuetify app
fengyu init scaffolds a Vue 3 + Vuetify app that builds into ui/. The generated src/main.ts delegates the complete bootstrap and teardown lifecycle to the shared UI kit:
import { fengyu } from '@infinia/plugin-sdk'
import { mountFengYuApp } from '@infinia/plugin-ui'
import '@infinia/plugin-ui/style.css'
import App from './App.vue'
if (!fengyu) throw new Error('FengYu SDK requires a browser environment')
// Bind to a local so the narrowed (non-undefined) type survives the
// top-level `await` below — TS widens imported `const` bindings across await.
const client = fengyu
await mountFengYuApp({ root: App, client })mountFengYuApp owns the complete UI lifecycle: it negotiates the environment once, synchronizes Vuetify theme/locale, injects the client, mounts the app, and unmounts/unsubscribes/disposes on pagehide. Pass plugins: [pinia, i18n] for extra Vue plugins and onEnvironment to synchronize plugin-specific message tables. Inside components, call useFengYuClient() instead of importing the singleton directly. The full component kit is documented on the UI Components page.
Legacy static plugins — a hand-written ui/index.html + ui/app.js that import { fengyu } from './sdk.js' — are still accepted. The rest of this page describes the SDK API that both entry styles share.
Sandboxed iframe + CSP
The host loads the entry HTML and applies a Content Security Policy that restricts what the MF may do. Consequences you will hit:
- No inline scripts. All JS must come from external
<script src>files (the scaffolder writes<script type="module" src="app.js">). See Pitfalls. - No direct
fetchto arbitrary origins. Talk to the backend throughFengYuClient, or use theapiBase+tokenprovided inPluginContextfor raw multipart calls. - Fonts must be packaged with the plugin.
font-src 'self' data:supports both current same-origin font assets and fonts embedded by older toolchain releases; remote font origins remain blocked.@infinia/plugin-uihandles MDI font packaging automatically. - Asset paths under
/plugin-runtime/{id}/**are the only plugin URLs that bypass the token filter, so the UI can bootstrap without a credential.
The FengYuClient API
FengYuClient (exported as the fengyu singleton) is a postMessage bridge to the host. Every method returns a Promise correlated by id; requests time out after 30s by default.
import { fengyu } from './sdk.js'
// 1. Negotiate protocol 2.0.0 and receive the host Environment.
const env = await fengyu.ready()
// → { protocolVersion, theme, locale, platform, capabilities }
// Throws unless the host speaks the exact protocol version.
// 2. Call the plugin's own worker (host forwards as JSON-RPC). A Vue plugin
// normally wraps this with the generated typed client:
// const rpc = createPluginRpc(fengyu); await rpc.render({ markdown: '# hi' })
const out = await fengyu.invoke('render', { markdown: '# hi' })
// 3. Ask the user for files / directories.
const file: FileRef | null = await fengyu.files.open({ extensions: ['xlsx'] })
const inDir: FileRef | null = await fengyu.files.inputDirectory()
const outDir: FileRef | null = await fengyu.files.outputDirectory()
const exported: boolean = await fengyu.files.export(outDir) // zip + download
// 4. Surface a host notification (requires the manifest's `notifications` permission).
await fengyu.notify('Split complete')
// 5. Subscribe to host events. Returns an unsubscribe function.
const off = fengyu.on('environment', (e) => applyTheme(e.theme))
// ...later
off()Method reference
| Method | Returns | Notes |
|---|---|---|
ready(options?) | Promise<Environment> | Negotiates exact protocol 2.0.0; concurrent calls share one handshake. Applies and caches theme + locale. |
currentEnvironment() | Environment | undefined | Latest merged ready/event state without another host round-trip. |
invoke(method, params?, options?) | Promise<T> | RPC to the plugin worker. options:{signal?, timeoutMs?}. |
notify(message) | Promise<boolean> | Creates a unified host notification (in-app toast, native OS notification when the window is hidden, and the persisted notification center). Requires the manifest permission notifications; undeclared calls resolve false and fall back to the iframe-local FyNotificationCenter. |
files.open({extensions?, filters?}, req?) | Promise<FileRef | null> | Open a single file. null if the user cancels. Perm files.read. |
files.inputDirectory(req?) | Promise<FileRef | null> | Pick an input directory. Perm files.read. |
files.outputDirectory(req?) | Promise<FileRef | null> | Allocate a writable output directory. Perm files.write. |
files.export(ref, req?) | Promise<boolean> | Zip an output dir and trigger a download. Perm files.write. |
on(event, handler) | () => void | Subscribe; returns an unsubscribe function. |
dispose() | void | Tear down listeners and reject pending requests. |
The supporting types:
type Theme = 'dark' | 'light'
type FileAccess = 'read' | 'write' | 'read-write'
interface FileRef { id: string; name: string; kind: 'file'|'directory'; access: FileAccess; size: number }
interface FileFilter { name: string; extensions: string[] }
interface Environment { protocolVersion: string; theme: Theme; locale: string; platform: 'web'|'desktop'; capabilities: HostMethod[] }Minimal ui/index.html
The scaffolder produces something close to this — an entry HTML with no inline script that hands off to app.js:
<!doctype html>
<html>
<body>
<h1>FengYu Plugin</h1>
<button id="hello">Call host</button>
<pre id="out"></pre>
<script type="module" src="app.js"></script>
</body>
</html>And the matching ui/app.js:
import { fengyu } from './sdk.js'
await fengyu.ready() // negotiate before any other call
document.querySelector('#hello').onclick = async () => {
const result = await fengyu.invoke('hello', {})
document.querySelector('#out').textContent = JSON.stringify(result, null, 2)
}Next steps
- UI Components — the
@infinia/plugin-uiVuetify kit the scaffolded app uses. - File I/O — what each
files.*method authorizes. - SDK & CLI — full TypeScript + Java SDK reference.
- Pitfalls — CSP, MF Vue dedupe, and FileRef timing.