single snapshot test
This commit is contained in:
98
src/bootstrap.ts
Normal file
98
src/bootstrap.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
export function runtimeBootstrapScript(basePath = "") {
|
||||
return `(() => {
|
||||
const runtime = {
|
||||
basePath: ${JSON.stringify(basePath)},
|
||||
hooks: {},
|
||||
mounts: {},
|
||||
registerMount(pluginId, hookName, mountId, mountFn) {
|
||||
const key = pluginId + ':' + hookName + ':' + mountId;
|
||||
this.mounts[key] = mountFn;
|
||||
},
|
||||
async loadScript(url) {
|
||||
if (document.querySelector('script[data-runtime-src="' + url + '"]')) return;
|
||||
await new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.async = true;
|
||||
script.dataset.runtimeSrc = url;
|
||||
script.onload = () => resolve();
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
},
|
||||
ensureCss(url) {
|
||||
if (!url || document.querySelector('link[data-runtime-css="' + url + '"]')) return;
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
link.dataset.runtimeCss = url;
|
||||
document.head.appendChild(link);
|
||||
},
|
||||
async fetchHooks() {
|
||||
const res = await fetch(this.basePath + '/api/runtime/hooks', { credentials: 'include' });
|
||||
if (!res.ok) throw new Error('hook fetch failed: ' + res.status);
|
||||
const data = await res.json();
|
||||
this.hooks = data.hooks || {};
|
||||
return data;
|
||||
},
|
||||
toolbarAnchor() {
|
||||
return document.querySelector('[data-mailcow-plugin-hook="mail.compose.toolbar"]')
|
||||
|| document.querySelector('.compose-toolbar')
|
||||
|| document.querySelector('[role="toolbar"]')
|
||||
|| document.body;
|
||||
},
|
||||
attachmentsAnchor() {
|
||||
return document.querySelector('[data-mailcow-plugin-hook="mail.compose.attachments"]')
|
||||
|| document.querySelector('.compose-attachments')
|
||||
|| document.querySelector('[data-attachments]')
|
||||
|| document.body;
|
||||
},
|
||||
async mountContribution(contribution) {
|
||||
if (contribution.cssUrl) this.ensureCss(contribution.cssUrl);
|
||||
if (contribution.mountUrl) await this.loadScript(contribution.mountUrl);
|
||||
const key = contribution.pluginId + ':' + contribution.hook + ':' + contribution.id;
|
||||
const mountFn = this.mounts[key];
|
||||
if (!mountFn) return;
|
||||
const parent = contribution.hook === 'mail.compose.attachments' ? this.attachmentsAnchor() : this.toolbarAnchor();
|
||||
let target = parent.querySelector('[data-plugin-target="' + key + '"]');
|
||||
if (!target) {
|
||||
target = document.createElement('div');
|
||||
target.dataset.pluginTarget = key;
|
||||
target.className = 'mailcow-plugin-target';
|
||||
parent.appendChild(target);
|
||||
}
|
||||
const context = {
|
||||
basePath: this.basePath,
|
||||
contribution,
|
||||
hooks: this.hooks,
|
||||
apiBase: contribution.apiBase,
|
||||
uiBase: contribution.uiBase,
|
||||
user: window.MAILCOW_USER || null,
|
||||
};
|
||||
await mountFn(target, context);
|
||||
},
|
||||
async renderHook(hookName) {
|
||||
const list = this.hooks[hookName] || [];
|
||||
for (const contribution of list) {
|
||||
try {
|
||||
await this.mountContribution(contribution);
|
||||
} catch (error) {
|
||||
console.error('Failed to mount contribution', contribution, error);
|
||||
}
|
||||
}
|
||||
},
|
||||
async init() {
|
||||
window.MAILCOW_PLUGIN_RUNTIME = this;
|
||||
await this.fetchHooks();
|
||||
await this.renderHook('mail.compose.toolbar');
|
||||
await this.renderHook('mail.compose.attachments');
|
||||
}
|
||||
};
|
||||
const start = () => runtime.init().catch((err) => console.error('Mailcow plugin runtime bootstrap failed', err));
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', start, { once: true });
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
})();`;
|
||||
}
|
||||
156
src/registry.ts
Normal file
156
src/registry.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import AjvModule from "ajv";
|
||||
const Ajv = (AjvModule as any).default || AjvModule;
|
||||
import { pluginManifestSchema } from "./schema.js";
|
||||
import type { HookName, PluginManifest, ResolvedHookContribution } from "./types.js";
|
||||
|
||||
export interface PluginRecord {
|
||||
manifest: PluginManifest;
|
||||
path: string;
|
||||
enabled: boolean;
|
||||
valid: boolean;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export class PluginRegistry {
|
||||
private ajv = new Ajv({ allErrors: true });
|
||||
private validate = this.ajv.compile(pluginManifestSchema);
|
||||
private plugins = new Map<string, PluginRecord>();
|
||||
private state = new Map<string, boolean>();
|
||||
|
||||
constructor(
|
||||
private pluginsDir: string,
|
||||
private stateFile: string,
|
||||
private basePath = "",
|
||||
) {
|
||||
this.loadState();
|
||||
}
|
||||
|
||||
private loadState() {
|
||||
this.state.clear();
|
||||
if (!fs.existsSync(this.stateFile)) return;
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(this.stateFile, "utf8")) as { enabled?: Record<string, boolean> };
|
||||
for (const [id, enabled] of Object.entries(raw.enabled || {})) {
|
||||
this.state.set(id, !!enabled);
|
||||
}
|
||||
} catch {
|
||||
// ignore corrupt state file and fall back to defaults
|
||||
}
|
||||
}
|
||||
|
||||
private saveState() {
|
||||
fs.mkdirSync(path.dirname(this.stateFile), { recursive: true });
|
||||
const enabled = Object.fromEntries([...this.state.entries()]);
|
||||
fs.writeFileSync(this.stateFile, JSON.stringify({ enabled }, null, 2));
|
||||
}
|
||||
|
||||
load(): PluginRecord[] {
|
||||
this.plugins.clear();
|
||||
this.loadState();
|
||||
if (!fs.existsSync(this.pluginsDir)) return [];
|
||||
const dirs = fs
|
||||
.readdirSync(this.pluginsDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
for (const dir of dirs) {
|
||||
const pluginPath = path.join(this.pluginsDir, dir);
|
||||
const manifestPath = path.join(pluginPath, "plugin.json");
|
||||
if (!fs.existsSync(manifestPath)) continue;
|
||||
try {
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as PluginManifest;
|
||||
const valid = this.validate(manifest);
|
||||
const enabled = this.state.has(manifest.id) ? !!this.state.get(manifest.id) : !!manifest.enabledByDefault;
|
||||
this.plugins.set(manifest.id, {
|
||||
manifest,
|
||||
path: pluginPath,
|
||||
enabled,
|
||||
valid: !!valid,
|
||||
errors: valid ? [] : (this.validate.errors || []).map((e: any) => `${e.instancePath || "/"} ${e.message}`),
|
||||
});
|
||||
} catch (err: any) {
|
||||
this.plugins.set(dir, {
|
||||
manifest: { id: dir, name: dir, version: "0.0.0" },
|
||||
path: pluginPath,
|
||||
enabled: false,
|
||||
valid: false,
|
||||
errors: [String(err?.message || err)],
|
||||
});
|
||||
}
|
||||
}
|
||||
return [...this.plugins.values()];
|
||||
}
|
||||
|
||||
list() {
|
||||
return [...this.plugins.values()];
|
||||
}
|
||||
|
||||
get(id: string) {
|
||||
return this.plugins.get(id);
|
||||
}
|
||||
|
||||
enable(id: string) {
|
||||
const r = this.plugins.get(id);
|
||||
if (r) {
|
||||
r.enabled = true;
|
||||
this.state.set(id, true);
|
||||
this.saveState();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
disable(id: string) {
|
||||
const r = this.plugins.get(id);
|
||||
if (r) {
|
||||
r.enabled = false;
|
||||
this.state.set(id, false);
|
||||
this.saveState();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
private resolveContribution(pluginId: string, manifest: PluginManifest, contribution: any): ResolvedHookContribution {
|
||||
const uiBase = `${this.basePath}/plugins/${pluginId}/ui`;
|
||||
const apiBase = `${this.basePath}/plugins/${pluginId}/api`;
|
||||
return {
|
||||
...contribution,
|
||||
pluginId,
|
||||
pluginName: manifest.name,
|
||||
pluginVersion: manifest.version,
|
||||
uiBase,
|
||||
apiBase,
|
||||
mountUrl: contribution.mount ? `${uiBase}/${contribution.mount.replace(/^\/+/, "")}` : undefined,
|
||||
cssUrl: contribution.css ? `${uiBase}/${contribution.css.replace(/^\/+/, "")}` : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
hooksIndex() {
|
||||
const m: Record<string, string[]> = {};
|
||||
for (const r of this.plugins.values()) {
|
||||
if (!r.enabled || !r.valid) continue;
|
||||
for (const h of r.manifest.hooks || []) {
|
||||
m[h] ||= [];
|
||||
m[h].push(r.manifest.id);
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
resolvedHooks() {
|
||||
const m: Record<string, ResolvedHookContribution[]> = {};
|
||||
for (const r of this.plugins.values()) {
|
||||
if (!r.enabled || !r.valid) continue;
|
||||
const contributions = r.manifest.contributions || {};
|
||||
for (const [hook, items] of Object.entries(contributions) as [HookName, any[]][]) {
|
||||
if (!items?.length) continue;
|
||||
m[hook] ||= [];
|
||||
for (const item of items) {
|
||||
m[hook].push(this.resolveContribution(r.manifest.id, r.manifest, item));
|
||||
}
|
||||
m[hook].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
}
|
||||
61
src/schema.ts
Normal file
61
src/schema.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
const hookNames = [
|
||||
"mail.compose.toolbar",
|
||||
"mail.compose.attachments",
|
||||
"mail.sidebar.apps",
|
||||
"admin.settings.panels",
|
||||
"auth.post_login",
|
||||
"message.pre_send",
|
||||
] as const;
|
||||
|
||||
export const pluginManifestSchema = {
|
||||
type: "object",
|
||||
required: ["id", "name", "version"],
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
id: { type: "string", minLength: 1 },
|
||||
name: { type: "string", minLength: 1 },
|
||||
version: { type: "string", minLength: 1 },
|
||||
description: { type: "string" },
|
||||
enabledByDefault: { type: "boolean" },
|
||||
permissions: { type: "array", items: { type: "string" } },
|
||||
hooks: { type: "array", items: { enum: [...hookNames] } },
|
||||
entrypoints: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
menu: { type: "string" },
|
||||
api: { type: "string" },
|
||||
ui: { type: "string" },
|
||||
},
|
||||
},
|
||||
contributions: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: Object.fromEntries(
|
||||
hookNames.map((hook) => [
|
||||
hook,
|
||||
{
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
required: ["hook", "type", "id", "label"],
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
hook: { const: hook },
|
||||
type: { enum: ["button", "panel", "modal", "script"] },
|
||||
id: { type: "string", minLength: 1 },
|
||||
label: { type: "string", minLength: 1 },
|
||||
icon: { type: "string" },
|
||||
description: { type: "string" },
|
||||
order: { type: "number" },
|
||||
mount: { type: "string" },
|
||||
css: { type: "string" },
|
||||
permissions: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
189
src/server.ts
Normal file
189
src/server.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import "dotenv/config";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { PluginRegistry } from "./registry.js";
|
||||
import { runtimeBootstrapScript } from "./bootstrap.js";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
const port = Number(process.env.PORT || 4110);
|
||||
const basePath = process.env.RUNTIME_BASE_PATH || "";
|
||||
const pluginsDir = path.resolve(process.env.PLUGINS_DIR || "./plugins");
|
||||
const dataDir = path.resolve(process.env.DATA_DIR || "./data");
|
||||
const stateFile = path.join(dataDir, "plugin-state.json");
|
||||
const allowOrigin = process.env.ALLOW_ORIGIN || "*";
|
||||
const owncloudBaseUrl = process.env.OWNCLOUD_BASE_URL || "https://owncloud.example.com";
|
||||
const owncloudUser = process.env.OWNCLOUD_DEMO_USER || "demo@peowco.com";
|
||||
|
||||
app.use(cors({ origin: allowOrigin === "*" ? true : allowOrigin, credentials: true }));
|
||||
|
||||
const registry = new PluginRegistry(pluginsDir, stateFile, basePath);
|
||||
registry.load();
|
||||
|
||||
function html(body: string, title = "Mailcow Plugin Runtime") {
|
||||
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title><style>
|
||||
body{font-family:Inter,Arial,sans-serif;margin:2rem;background:#fafafa;color:#111}
|
||||
.card{border:1px solid #ddd;border-radius:12px;padding:1rem;margin-bottom:1rem;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.04)}
|
||||
code,pre{background:#f5f5f5;padding:2px 4px;border-radius:6px} pre{padding:1rem;overflow:auto}
|
||||
.row{display:flex;gap:1rem;flex-wrap:wrap}.pill{display:inline-block;padding:.2rem .5rem;border:1px solid #ddd;border-radius:999px;margin:.1rem .2rem .1rem 0}
|
||||
button{border:1px solid #333;background:#111;color:#fff;padding:.5rem .75rem;border-radius:8px;cursor:pointer}
|
||||
.muted{color:#555}
|
||||
.toolbar-anchor,.attachments-anchor{padding:.75rem;border:1px dashed #bbb;border-radius:10px;background:#fcfcfc;margin:.75rem 0}
|
||||
</style></head><body>${body}</body></html>`;
|
||||
}
|
||||
|
||||
function pluginUiDir(pluginId: string) {
|
||||
return path.join(pluginsDir, pluginId, "ui");
|
||||
}
|
||||
|
||||
function pluginRootDir(pluginId: string) {
|
||||
return path.join(pluginsDir, pluginId);
|
||||
}
|
||||
|
||||
function sanitizePluginId(pluginId: string) {
|
||||
return /^[a-z0-9][a-z0-9-]*$/i.test(pluginId) ? pluginId : null;
|
||||
}
|
||||
|
||||
app.get(`${basePath}/health`, (_req, res) =>
|
||||
res.json({ ok: true, version: "0.2.0", pluginsDir, dataDir, basePath, owncloudBaseUrl }),
|
||||
);
|
||||
|
||||
app.get(`${basePath}/api/runtime/plugins`, (_req, res) => {
|
||||
registry.load();
|
||||
res.json({ ok: true, version: "0.2.0", plugins: registry.list() });
|
||||
});
|
||||
|
||||
app.get(`${basePath}/api/runtime/hooks`, (_req, res) => {
|
||||
registry.load();
|
||||
res.json({ ok: true, version: "0.2.0", hooks: registry.resolvedHooks(), index: registry.hooksIndex() });
|
||||
});
|
||||
|
||||
app.post(`${basePath}/api/runtime/plugins/:id/enable`, (req, res) => {
|
||||
registry.load();
|
||||
const p = registry.enable(req.params.id);
|
||||
if (!p) return res.status(404).json({ ok: false, error: "plugin_not_found" });
|
||||
res.json({ ok: true, plugin: p });
|
||||
});
|
||||
|
||||
app.post(`${basePath}/api/runtime/plugins/:id/disable`, (req, res) => {
|
||||
registry.load();
|
||||
const p = registry.disable(req.params.id);
|
||||
if (!p) return res.status(404).json({ ok: false, error: "plugin_not_found" });
|
||||
res.json({ ok: true, plugin: p });
|
||||
});
|
||||
|
||||
app.get(`${basePath}/runtime/bootstrap.js`, (_req, res) =>
|
||||
res.type("application/javascript").send(runtimeBootstrapScript(basePath)),
|
||||
);
|
||||
|
||||
app.use(`${basePath}/plugins/:pluginId/ui`, (req, res, next) => {
|
||||
const pluginId = sanitizePluginId(req.params.pluginId);
|
||||
if (!pluginId) return res.status(400).send("invalid plugin id");
|
||||
return express.static(pluginUiDir(pluginId))(req, res, next);
|
||||
});
|
||||
|
||||
app.get(`${basePath}/plugins/:pluginId/manifest`, (req, res) => {
|
||||
registry.load();
|
||||
const pluginId = sanitizePluginId(req.params.pluginId);
|
||||
if (!pluginId) return res.status(400).json({ ok: false, error: "invalid_plugin_id" });
|
||||
const record = registry.get(pluginId);
|
||||
if (!record) return res.status(404).json({ ok: false, error: "plugin_not_found" });
|
||||
res.json({ ok: true, plugin: record });
|
||||
});
|
||||
|
||||
app.get(`${basePath}/plugins/owncloud-attach/api/me`, (_req, res) => {
|
||||
res.json({
|
||||
ok: true,
|
||||
user: {
|
||||
id: owncloudUser,
|
||||
email: owncloudUser,
|
||||
displayName: process.env.OWNCLOUD_DEMO_DISPLAY_NAME || "Peowco Demo User",
|
||||
owncloudBaseUrl,
|
||||
authMode: process.env.OWNCLOUD_AUTH_MODE || "oidc-proxy",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
app.get(`${basePath}/plugins/owncloud-attach/api/files`, (req, res) => {
|
||||
const requestedPath = String(req.query.path || "/");
|
||||
const mockFilesPath = path.join(pluginRootDir("owncloud-attach"), "data", "files.json");
|
||||
const sample = fs.existsSync(mockFilesPath)
|
||||
? JSON.parse(fs.readFileSync(mockFilesPath, "utf8"))
|
||||
: { cwd: requestedPath, items: [] };
|
||||
res.json({
|
||||
ok: true,
|
||||
source: process.env.OWNCLOUD_LISTING_MODE || "mock",
|
||||
cwd: requestedPath,
|
||||
items: sample.items || [],
|
||||
});
|
||||
});
|
||||
|
||||
app.post(`${basePath}/plugins/owncloud-attach/api/select`, (req, res) => {
|
||||
const body = req.body || {};
|
||||
const files = Array.isArray(body.files) ? body.files : [];
|
||||
res.json({
|
||||
ok: true,
|
||||
mode: process.env.OWNCLOUD_ATTACHMENT_MODE || "draft-upload-proxy",
|
||||
selectedCount: files.length,
|
||||
files: files.map((file: any) => ({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
status: "queued",
|
||||
mailcowDraftUpload: {
|
||||
recommendedEndpoint: process.env.MAILCOW_DRAFT_UPLOAD_ENDPOINT || "/api/v1/mail/attachments/upload",
|
||||
method: "POST",
|
||||
},
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
app.get(`${basePath}/runtime/ui`, (_req, res) => {
|
||||
registry.load();
|
||||
const plugins = registry.list();
|
||||
const hooks = registry.resolvedHooks();
|
||||
res.type("html").send(
|
||||
html(`
|
||||
<h1>Mailcow Plugin Runtime v0.2.0</h1>
|
||||
<p class="muted">Combined foundation: v0.1.0 registry/runtime + v0.2.0 rendered hook contributions, plugin asset serving, persisted plugin state, and ownCloud reference plugin UI/API.</p>
|
||||
<div class="row">
|
||||
<div class="card"><strong>Base path</strong><div><code>${basePath || "/"}</code></div></div>
|
||||
<div class="card"><strong>Plugins dir</strong><div><code>${pluginsDir}</code></div></div>
|
||||
<div class="card"><strong>State file</strong><div><code>${stateFile}</code></div></div>
|
||||
</div>
|
||||
<h2>Plugins</h2>
|
||||
${plugins
|
||||
.map(
|
||||
(p) => `<div class="card"><h3>${p.manifest.name}</h3>
|
||||
<div><strong>ID:</strong> <code>${p.manifest.id}</code></div>
|
||||
<div><strong>Version:</strong> ${p.manifest.version}</div>
|
||||
<div><strong>Enabled:</strong> ${p.enabled}</div>
|
||||
<div><strong>Valid:</strong> ${p.valid}</div>
|
||||
<div><strong>Hooks:</strong> ${(Object.keys(p.manifest.contributions || {}).join(", ") || (p.manifest.hooks || []).join(", ") || "none")}</div>
|
||||
</div>`,
|
||||
)
|
||||
.join("")}
|
||||
<h2>Runtime demo</h2>
|
||||
<div class="toolbar-anchor" data-mailcow-plugin-hook="mail.compose.toolbar"><strong>Compose toolbar hook target</strong></div>
|
||||
<div class="attachments-anchor" data-mailcow-plugin-hook="mail.compose.attachments"><strong>Compose attachments hook target</strong></div>
|
||||
<script src="${basePath}/runtime/bootstrap.js"></script>
|
||||
<h2>Resolved hooks</h2>
|
||||
<pre>${JSON.stringify(hooks, null, 2)}</pre>
|
||||
`),
|
||||
);
|
||||
});
|
||||
|
||||
app.get(`${basePath}/mailcow/nginx.conf`, (_req, res) => {
|
||||
res.type("text/plain").send(`location ${basePath || "/plugins-runtime/"} {
|
||||
proxy_pass http://mailcow-plugin-runtime:${port}${basePath || "/plugins-runtime/"};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}`);
|
||||
});
|
||||
|
||||
app.listen(port, () => console.log(`mailcow-plugin-runtime v0.2.0 listening on ${port}`));
|
||||
50
src/types.ts
Normal file
50
src/types.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export type HookName =
|
||||
| "mail.compose.toolbar"
|
||||
| "mail.compose.attachments"
|
||||
| "mail.sidebar.apps"
|
||||
| "admin.settings.panels"
|
||||
| "auth.post_login"
|
||||
| "message.pre_send";
|
||||
|
||||
export type ContributionType = "button" | "panel" | "modal" | "script";
|
||||
|
||||
export interface PluginEntrypoints {
|
||||
menu?: string;
|
||||
api?: string;
|
||||
ui?: string;
|
||||
}
|
||||
|
||||
export interface HookContribution {
|
||||
hook: HookName;
|
||||
type: ContributionType;
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
order?: number;
|
||||
mount?: string;
|
||||
css?: string;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export interface PluginManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
entrypoints?: PluginEntrypoints;
|
||||
permissions?: string[];
|
||||
hooks?: HookName[];
|
||||
contributions?: Partial<Record<HookName, HookContribution[]>>;
|
||||
enabledByDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface ResolvedHookContribution extends HookContribution {
|
||||
pluginId: string;
|
||||
pluginName: string;
|
||||
pluginVersion: string;
|
||||
apiBase: string;
|
||||
uiBase: string;
|
||||
mountUrl?: string;
|
||||
cssUrl?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user