Archived
Load Pi Web plugins from server manifest
This commit is contained in:
@@ -11,7 +11,7 @@ import { WorkspaceController } from "../controllers/workspaceController";
|
||||
import { KeyboardShortcutDispatcher } from "../keyboardShortcuts";
|
||||
import type { QualifiedContributionId, QualifiedWorkspacePanelContribution, PluginRuntimeContext } from "../plugins/types";
|
||||
import { corePlugin } from "../plugins/core";
|
||||
import { examplePlugin } from "../plugins/example";
|
||||
import { loadExternalPlugins } from "../plugins/external";
|
||||
import { PluginRegistry } from "../plugins/registry";
|
||||
import { queryNamespace, readNamespacedString } from "../namespacedQueryArgs";
|
||||
import { readRoute, writeRoute } from "../route";
|
||||
@@ -76,6 +76,7 @@ export class PiWebApp extends LitElement {
|
||||
window.addEventListener("popstate", this.onPopState);
|
||||
window.addEventListener("keydown", this.onKeyDown);
|
||||
this.sessions.connectStatusUpdates();
|
||||
void this.loadExternalPlugins();
|
||||
void this.loadProjectsAndRestoreRoute();
|
||||
}
|
||||
|
||||
@@ -215,6 +216,15 @@ export class PiWebApp extends LitElement {
|
||||
return this.plugins.getActions(this.createPluginRuntimeContext());
|
||||
}
|
||||
|
||||
private async loadExternalPlugins(): Promise<void> {
|
||||
try {
|
||||
for (const plugin of await loadExternalPlugins()) this.plugins.register(plugin);
|
||||
this.requestUpdate();
|
||||
} catch (error) {
|
||||
console.warn("Failed to load external Pi Web plugins", error);
|
||||
}
|
||||
}
|
||||
|
||||
private createPluginRuntimeContext(): PluginRuntimeContext {
|
||||
return {
|
||||
state: this.state,
|
||||
@@ -272,7 +282,6 @@ export class PiWebApp extends LitElement {
|
||||
function createPluginRegistry(): PluginRegistry {
|
||||
const registry = new PluginRegistry();
|
||||
registry.register(corePlugin);
|
||||
registry.register(examplePlugin);
|
||||
return registry;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { html } from "lit";
|
||||
import type { PiWebPlugin } from "./types";
|
||||
|
||||
interface PluginManifestEntry {
|
||||
module: string;
|
||||
}
|
||||
|
||||
interface PluginManifest {
|
||||
plugins: PluginManifestEntry[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
piWebPluginApi?: {
|
||||
apiVersion: 1;
|
||||
html: typeof html;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadExternalPlugins(manifestUrl = "/pi-web-plugins/manifest.json"): Promise<PiWebPlugin[]> {
|
||||
window.piWebPluginApi = { apiVersion: 1, html };
|
||||
|
||||
const manifest = await fetchPluginManifest(manifestUrl);
|
||||
if (manifest === undefined) return [];
|
||||
|
||||
const plugins: PiWebPlugin[] = [];
|
||||
for (const entry of manifest.plugins) {
|
||||
try {
|
||||
const moduleUrl = new URL(entry.module, new URL(manifestUrl, window.location.href)).toString();
|
||||
const module: unknown = await import(/* @vite-ignore */ moduleUrl);
|
||||
const plugin = parsePluginModule(module, moduleUrl);
|
||||
if (plugin !== undefined) plugins.push(plugin);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load Pi Web plugin ${entry.module}`, error);
|
||||
}
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
async function fetchPluginManifest(manifestUrl: string): Promise<PluginManifest | undefined> {
|
||||
const response = await fetch(manifestUrl, { cache: "no-store" });
|
||||
if (response.status === 404) return undefined;
|
||||
if (!response.ok) throw new Error(`Failed to load plugin manifest: ${response.statusText}`);
|
||||
return parseManifest(await response.json());
|
||||
}
|
||||
|
||||
function parseManifest(value: unknown): PluginManifest {
|
||||
if (!isRecord(value) || !Array.isArray(value["plugins"])) throw new Error("Invalid plugin manifest");
|
||||
return {
|
||||
plugins: value["plugins"].map((entry) => {
|
||||
if (!isRecord(entry) || typeof entry["module"] !== "string" || entry["module"] === "") throw new Error("Invalid plugin manifest entry");
|
||||
return { module: entry["module"] };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function parsePluginModule(module: unknown, moduleUrl: string): PiWebPlugin | undefined {
|
||||
if (!isRecord(module)) throw new Error(`Plugin module ${moduleUrl} did not export an object`);
|
||||
const plugin = module["default"];
|
||||
if (!isPiWebPlugin(plugin)) throw new Error(`Plugin module ${moduleUrl} default export is not a PiWebPlugin`);
|
||||
return plugin;
|
||||
}
|
||||
|
||||
function isPiWebPlugin(value: unknown): value is PiWebPlugin {
|
||||
return isRecord(value) && typeof value["id"] === "string" && typeof value["name"] === "string" && typeof value["activate"] === "function";
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
Reference in New Issue
Block a user