feat: plugin hot-reload + dev-folder loading

This commit is contained in:
Linus Rath
2026-05-05 18:05:17 +02:00
parent 94f55afd1f
commit 3e336d459c
6 changed files with 267 additions and 33 deletions
+21 -3
View File
@@ -1,5 +1,6 @@
import { readFile, writeFile, mkdir, rename, unlink } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import path from 'node:path';
import { logger } from '@/lib/logger';
@@ -41,6 +42,12 @@ export interface ServerPlugin {
configSchema?: Record<string, PluginConfigField>;
installedAt: string;
updatedAt: string;
/**
* SHA-256 hex of the bundle code (first 16 chars). Refreshed every save so
* the same version re-uploaded with new code still appears as a change to
* the client. Also doubles as the HTTP ETag for the bundle endpoint.
*/
bundleHash?: string;
/**
* Validated CSP origins (https-only, single-origin form) the plugin may
* embed. Merged into the host frame-src by the proxy.
@@ -120,13 +127,24 @@ export async function savePlugin(
const bundlePath = path.join(dir, `${plugin.id}.js`);
await writeFile(bundlePath, code, 'utf-8');
// Update registry
// Stamp content hash + updatedAt so clients can detect re-uploads even
// when the manifest version hasn't changed. Preserve the original
// installedAt across re-uploads.
const bundleHash = createHash('sha256').update(code).digest('hex').slice(0, 16);
const now = new Date().toISOString();
const registry = await getPluginRegistry();
const idx = registry.plugins.findIndex(p => p.id === plugin.id);
const next: ServerPlugin = {
...plugin,
bundleHash,
updatedAt: now,
installedAt: idx >= 0 ? registry.plugins[idx].installedAt : plugin.installedAt,
};
if (idx >= 0) {
registry.plugins[idx] = plugin;
registry.plugins[idx] = next;
} else {
registry.plugins.push(plugin);
registry.plugins.push(next);
}
await writeJsonFile(pluginRegistryPath(), registry);
}