Fix ZIP download URL, add activate/deactivate compat shim
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Evaluates plugin source code and returns the plugin object.
|
||||
* Evaluates plugin source code and returns the raw module exports.
|
||||
* Tries CJS eval first; on SyntaxError falls back to ESM data-URL import.
|
||||
* @param {string} code
|
||||
* @returns {Promise<object>}
|
||||
@@ -45,6 +45,78 @@
|
||||
return mod.default ?? mod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalises a raw plugin module to our Plugin interface.
|
||||
*
|
||||
* Handles two formats:
|
||||
* - Native: `{ onLoad(ctx), onUnload() }` — used as-is
|
||||
* - Legacy: named exports `activate(ctx)` / `deactivate(ctx)` — wrapped into
|
||||
* onLoad/onUnload with a stub context that no-ops unsupported calls so the
|
||||
* plugin doesn't crash on missing methods (registerHook, runHook, getConfig).
|
||||
*
|
||||
* @param {object} raw Raw module exports
|
||||
* @returns {object} Plugin-interface-compatible object
|
||||
*/
|
||||
function normalisePlugin(raw) {
|
||||
if (typeof raw.onLoad === 'function') return raw; // already native
|
||||
|
||||
if (typeof raw.activate === 'function') {
|
||||
return {
|
||||
name: raw.name ?? undefined,
|
||||
version: raw.version ?? undefined,
|
||||
onLoad: async (ctx) => {
|
||||
// Extend ctx with legacy methods so the plugin doesn't throw
|
||||
const legacyCtx = Object.assign(Object.create(ctx), {
|
||||
registerHook: (name, fn) => ctx.log(`[compat] registerHook("${name}") — not supported`),
|
||||
runHook: (name, data) => { ctx.log(`[compat] runHook("${name}")`); return Promise.resolve([]); },
|
||||
getConfig: (key) => ctx.settings.get(key),
|
||||
on: (event, handler) => ctx.events.on(event, handler),
|
||||
});
|
||||
await raw.activate(legacyCtx);
|
||||
},
|
||||
onUnload: raw.deactivate
|
||||
? async (ctx) => raw.deactivate(ctx)
|
||||
: undefined,
|
||||
exports: raw,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Plugin does not export onLoad or activate — cannot load');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches plugin JS source from a URL.
|
||||
* If the URL points to a ZIP archive, derives the raw index.js URL from the
|
||||
* repository field in the plugin metadata (Gitea/GitHub raw file convention).
|
||||
*
|
||||
* @param {string} downloadUrl
|
||||
* @param {string} [repositoryUrl] Fallback repo root for ZIP-only entries
|
||||
* @returns {Promise<string>} JS source code
|
||||
*/
|
||||
async function fetchPluginCode(downloadUrl, repositoryUrl) {
|
||||
const isZip = /\.zip($|\?)/.test(downloadUrl);
|
||||
|
||||
if (!isZip) {
|
||||
const res = await fetch(downloadUrl);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${downloadUrl}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
// ZIP URL — try to derive a raw index.js URL from the repository root
|
||||
if (!repositoryUrl) throw new Error('Download URL is a ZIP and no repository URL is available');
|
||||
|
||||
// Supports Gitea (/raw/branch/main/) and GitHub (/raw/main/) conventions
|
||||
const giteaRaw = `${repositoryUrl.replace(/\/$/, '')}/raw/branch/main/index.js`;
|
||||
const res = await fetch(giteaRaw);
|
||||
if (!res.ok) {
|
||||
const ghRaw = `${repositoryUrl.replace(/\/$/, '')}/raw/main/index.js`;
|
||||
const res2 = await fetch(ghRaw);
|
||||
if (!res2.ok) throw new Error(`Could not fetch index.js from ${repositoryUrl} (tried Gitea and GitHub raw URL patterns)`);
|
||||
return res2.text();
|
||||
}
|
||||
return res.text();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Logging helper — writes to the on-screen log panel
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -234,7 +306,8 @@
|
||||
async function loadPluginFromPath(filePath) {
|
||||
try {
|
||||
const code = fs.readFileSync(filePath, 'utf-8');
|
||||
const p = await evalPluginCode(code);
|
||||
const raw = await evalPluginCode(code);
|
||||
const p = normalisePlugin(raw);
|
||||
|
||||
if (!p || typeof p.onLoad !== 'function') {
|
||||
throw new Error('File does not export a valid plugin (needs an onLoad function)');
|
||||
@@ -344,6 +417,7 @@
|
||||
style="font-size:10px;color:#7986cb;text-decoration:none;margin-left:auto;margin-right:6px;">↗ Homepage</a>` : ''}
|
||||
<button class="market-install-btn"
|
||||
data-url="${p.downloadUrl ?? ''}"
|
||||
data-repo="${p.repository ?? ''}"
|
||||
data-id="${p.id}"
|
||||
${isLoaded ? 'disabled' : ''}>
|
||||
${isLoaded ? 'Loaded' : 'Download'}
|
||||
@@ -354,27 +428,21 @@
|
||||
}).join('');
|
||||
|
||||
dirListEl.querySelectorAll('.market-install-btn:not([disabled])').forEach(btn => {
|
||||
btn.addEventListener('click', () => downloadAndLoadPlugin(btn.dataset.id, btn.dataset.url));
|
||||
btn.addEventListener('click', () => downloadAndLoadPlugin(btn.dataset.id, btn.dataset.url, btn.dataset.repo));
|
||||
});
|
||||
}
|
||||
|
||||
/** Downloads a plugin's index.js from the marketplace and loads it into the registry. */
|
||||
async function downloadAndLoadPlugin(pluginId, downloadUrl) {
|
||||
async function downloadAndLoadPlugin(pluginId, downloadUrl, repositoryUrl) {
|
||||
if (!downloadUrl) {
|
||||
addLog(`No download URL for plugin ${pluginId}`, 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
addLog(`Downloading plugin ${pluginId}…`, 'info');
|
||||
const res = await fetch(downloadUrl);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${downloadUrl}`);
|
||||
const code = await res.text();
|
||||
|
||||
const p = await evalPluginCode(code);
|
||||
|
||||
if (!p || typeof p.onLoad !== 'function') {
|
||||
throw new Error(`Plugin ${pluginId} does not export a valid plugin object`);
|
||||
}
|
||||
const code = await fetchPluginCode(downloadUrl, repositoryUrl);
|
||||
const raw = await evalPluginCode(code);
|
||||
const p = normalisePlugin(raw);
|
||||
|
||||
await registerAndLoad(pluginId, p, { downloadUrl });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user