Barazo default frontend barazo.forum

feat(plugins): wire up plugin frontend components and fix option labels (#199)

* feat(plugins): wire up plugin frontend components and fix option labels

- Add plugin frontend loader that dynamically imports bundled plugin
register functions and wires them into the slot registry
- Call loader from PluginProvider after fetching enabled plugins
- Add @barazo/plugin-signatures as workspace dependency
- Add transpilePackages for plugin packages in next.config
- Format select option labels in SettingsField (replace underscores,
capitalize words) so "first_per_thread" displays as "First Per Thread"

* fix(plugins): use variable import path to avoid CI typecheck failure

The static import('@barazo/plugin-signatures/frontend/register') made
TypeScript resolve the module at compile time, which fails in CI where
the workspace plugin package isn't available. Using a variable path
bypasses static resolution while keeping the explicit return type.

authored by

Guido X Jansen and committed by
GitHub
0473f9b6 fb93d320

+73 -1
+3
next.config.ts
··· 9 9 // Standalone output for Docker (includes Node.js server) 10 10 output: 'standalone', 11 11 12 + // Transpile workspace plugin packages so Next.js bundles their frontend code 13 + transpilePackages: ['@barazo/plugin-signatures'], 14 + 12 15 // Image optimization — allow any HTTPS source for AT Protocol PDS avatars/banners. 13 16 // Self-hosted PDS instances can use arbitrary domains, so a wildcard is necessary. 14 17 images: {
+1
package.json
··· 29 29 "prepare": "husky" 30 30 }, 31 31 "dependencies": { 32 + "@barazo/plugin-signatures": "link:../barazo-plugins/packages/plugin-signatures", 32 33 "@singi-labs/lexicons": "link:../barazo-lexicons", 33 34 "@dnd-kit/core": "6.3.1", 34 35 "@dnd-kit/sortable": "10.0.0",
+3
pnpm-lock.yaml
··· 38 38 39 39 .: 40 40 dependencies: 41 + '@barazo/plugin-signatures': 42 + specifier: link:../barazo-plugins/packages/plugin-signatures 43 + version: link:../barazo-plugins/packages/plugin-signatures 41 44 '@dnd-kit/core': 42 45 specifier: 6.3.1 43 46 version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+5 -1
src/components/admin/plugins/settings-field.tsx
··· 6 6 7 7 import type { PluginSettingsSchema } from '@/lib/api/types' 8 8 9 + function formatOptionLabel(value: string): string { 10 + return value.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) 11 + } 12 + 9 13 interface SettingsFieldProps { 10 14 fieldKey: string 11 15 schema: PluginSettingsSchema[string] ··· 47 51 > 48 52 {schema.options?.map((opt) => ( 49 53 <option key={opt} value={opt}> 50 - {opt || '(none)'} 54 + {opt ? formatOptionLabel(opt) : '(none)'} 51 55 </option> 52 56 ))} 53 57 </select>
+3
src/context/plugin-context.tsx
··· 11 11 import type { Plugin } from '@/lib/api/types' 12 12 import { getPlugins } from '@/lib/api/client' 13 13 import { useAuth } from '@/hooks/use-auth' 14 + import { loadBundledPlugins } from '@/lib/plugins/loader' 14 15 15 16 export interface PluginContextValue { 16 17 /** List of all plugins (enabled and disabled) */ ··· 48 49 try { 49 50 const response = await getPlugins(token) 50 51 setPlugins(response.plugins) 52 + const enabledNames = response.plugins.filter((p) => p.enabled).map((p) => p.name) 53 + void loadBundledPlugins(enabledNames) 51 54 } catch { 52 55 // On error, keep existing plugins (or empty on first load) 53 56 setPlugins((prev) => prev)
+58
src/lib/plugins/loader.ts
··· 1 + /** 2 + * Plugin frontend loader. 3 + * Registers bundled plugin components into the slot registry. 4 + * Called once by PluginProvider after the plugin list is fetched. 5 + */ 6 + 7 + import type { ComponentType } from 'react' 8 + import { registerPluginComponent } from './registry' 9 + import type { SlotName } from './registry' 10 + 11 + interface PluginComponentRegistry { 12 + add: (slot: string, component: ComponentType<Record<string, unknown>>) => void 13 + } 14 + 15 + function createRegistryAdapter(pluginName: string): PluginComponentRegistry { 16 + return { 17 + add(slot: string, component: ComponentType<Record<string, unknown>>) { 18 + registerPluginComponent(slot as SlotName, pluginName, component) 19 + }, 20 + } 21 + } 22 + 23 + // Map of bundled plugin names to their frontend register module paths. 24 + // Dynamic import uses a variable so TypeScript doesn't statically resolve the 25 + // module — this lets CI pass without the workspace plugin packages checked out. 26 + const BUNDLED_PLUGIN_PATHS: Record<string, string> = { 27 + '@barazo/plugin-signatures': '@barazo/plugin-signatures/frontend/register', 28 + } 29 + 30 + function pluginLoader( 31 + modulePath: string 32 + ): () => Promise<{ register: (r: PluginComponentRegistry) => void }> { 33 + return () => import(/* webpackIgnore: true */ modulePath) 34 + } 35 + 36 + const BUNDLED_PLUGINS: Record< 37 + string, 38 + () => Promise<{ register: (r: PluginComponentRegistry) => void }> 39 + > = Object.fromEntries( 40 + Object.entries(BUNDLED_PLUGIN_PATHS).map(([name, path]) => [name, pluginLoader(path)]) 41 + ) 42 + 43 + let loaded = false 44 + 45 + export async function loadBundledPlugins(enabledPluginNames: string[]): Promise<void> { 46 + if (loaded) return 47 + loaded = true 48 + 49 + for (const [name, loader] of Object.entries(BUNDLED_PLUGINS)) { 50 + if (!enabledPluginNames.includes(name)) continue 51 + try { 52 + const mod = await loader() 53 + mod.register(createRegistryAdapter(name)) 54 + } catch { 55 + // Plugin failed to load — silently skip (error boundary catches render errors) 56 + } 57 + } 58 + }